BigInt
// return the result
let limbs = FixedArray::make(max_length, 0U)
limbs.unsafe_blit(0, x_limbs, 0, max_length)
- { limbs, sign: new_sign, len: normalize_len(limbs, max_length) }
+ { limbs, sign: new_sign, len: normalize_len(limbs, max_length), }
}
///|
@@ -1716,7 +1755,7 @@ pub impl BitOr for BigInt with fn lor(self : BigInt, other : BigInt) -> BigInt {
// return the result
let limbs = FixedArray::make(max_length, 0U)
limbs.unsafe_blit(0, x_limbs, 0, max_length)
- { limbs, sign: new_sign, len: normalize_len(limbs, max_length) }
+ { limbs, sign: new_sign, len: normalize_len(limbs, max_length), }
}
///|
@@ -1806,7 +1845,7 @@ pub impl BitXOr for BigInt with fn lxor(self : BigInt, other : BigInt) -> BigInt
// return the result
let limbs = FixedArray::make(max_length, 0U)
limbs.unsafe_blit(0, x_limbs, 0, max_length)
- { limbs, sign: new_sign, len: normalize_len(limbs, max_length) }
+ { limbs, sign: new_sign, len: normalize_len(limbs, max_length), }
}
///|
@@ -1851,8 +1890,15 @@ pub fn BigInt::to_int(self : BigInt) -> Int {
/// }
/// ```
pub fn BigInt::to_uint(self : BigInt) -> UInt {
- let value = if self.sign == Negative { (1N << 32) + self } else { self }
- value.limbs[0]
+ // Truncation to the low 32 bits is a ring homomorphism, so for a
+ // negative value it is the wrapping negation of the magnitude's low
+ // limb. (Adding one modulus and reading the limb would be wrong
+ // whenever the sum is still negative, i.e. whenever |self| > 2^32.)
+ if self.sign == Negative {
+ 0U - self.limbs[0]
+ } else {
+ self.limbs[0]
+ }
}
///|
@@ -1860,7 +1906,7 @@ pub fn BigInt::to_uint(self : BigInt) -> UInt {
///
/// Parameters:
///
-/// * `value` : The `BigInt` value to be converted.
+/// * `self` : The `BigInt` value to be converted.
///
/// Returns a 64-bit signed integer (`Int64`) representing the lower 64 bits of
/// the input `BigInt`.
@@ -1900,15 +1946,22 @@ pub fn BigInt::to_int64(self : BigInt) -> Int64 {
/// }
/// ```
pub fn BigInt::to_uint64(self : BigInt) -> UInt64 {
- let value = if self.sign == Negative { (1N << 64) + self } else { self }
let len = 64 / RADIX_BIT_LEN
- let len = if value.len < len { value.len } else { len }
+ let len = if self.len < len { self.len } else { len }
let mut result = 0UL
for i in len>..0 {
result = result << RADIX_BIT_LEN
- result = result | (value.limbs[i].to_uint64() & RADIX_MASK)
+ result = result | (self.limbs[i].to_uint64() & RADIX_MASK)
+ }
+ // Truncation to the low 64 bits is a ring homomorphism, so for a
+ // negative value it is the wrapping negation of the magnitude's low
+ // bits. (Adding one modulus and reading the limbs would be wrong
+ // whenever the sum is still negative, i.e. whenever |self| > 2^64.)
+ if self.sign == Negative {
+ 0UL - result
+ } else {
+ result
}
- result
}
///|
diff --git a/bigint/bigint_nonjs_wbtest.mbt b/bigint/bigint_default_wbtest.mbt
similarity index 89%
rename from bigint/bigint_nonjs_wbtest.mbt
rename to bigint/bigint_default_wbtest.mbt
index 4eeaa35b5a..dfdfbce603 100644
--- a/bigint/bigint_nonjs_wbtest.mbt
+++ b/bigint/bigint_default_wbtest.mbt
@@ -12,14 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+// White-box tests for the default 32-bit-limb implementation.
+
///|
-struct MyBigInt(BigInt)
+priv struct MyBigInt(BigInt)
///|
impl Show for MyBigInt with fn output(self, logger) {
- logger.write_string(
- "{limbs : \{@debug.Repr(self.limbs)}, sign : \{@debug.Repr(self.sign)}, len : \{self.len} }",
- )
+ logger <+
+ "{limbs : \{@debug.Repr(self.limbs)}, sign : \{@debug.Repr(self.sign)}, len : \{self.len} }"
}
///|
@@ -183,3 +184,16 @@ test {
content="{limbs :
, sign : Negative, len : 2 }",
) // Int64.min_value - 2
}
+
+///|
+/// `BigInt::from_octets` reads each tail limb with a single `u32be` bits
+/// pattern, which agrees with the shift-accumulate loop it replaced only when a
+/// limb is exactly four bytes wide. Pin the constant here so narrowing it fails
+/// this test instead of silently mis-decoding octets.
+///
+/// Deliberately `assert_eq` rather than `inspect`: a snapshot would be rewritten
+/// by `moon test --update`, which would silently retire this guard at exactly
+/// the moment it is supposed to fire.
+test "from_octets assumes 32-bit limbs" {
+ @test.assert_eq(RADIX_BIT_LEN, 32)
+}
diff --git a/bigint/bigint_js.mbt b/bigint/bigint_js.mbt
index fce60c99eb..8978e1e96c 100644
--- a/bigint/bigint_js.mbt
+++ b/bigint/bigint_js.mbt
@@ -199,12 +199,15 @@ extern "js" fn hex2(b : Byte) -> String =
/// Parameters:
///
/// * `octets` : A sequence of bytes representing the magnitude of the number in
-/// big-endian order. It must not be empty unless `signum` is 0.
+/// big-endian order. An empty sequence represents a zero magnitude.
/// * `signum` : The sign of the resulting number. A negative value creates a
/// negative number, zero returns zero, and a positive value creates a positive
/// number. Defaults to 1.
///
/// Returns a `BigInt` value represented by the byte sequence and sign.
+///
+/// An empty byte sequence yields zero for any `signum`, matching the behavior
+/// of Java's `BigInteger`, Python's `int.from_bytes`, and Rust's `num-bigint`.
pub fn BigInt::from_octets(octets : BytesView, signum? : Int = 1) -> BigInt {
if signum < 0 {
return -1N * BigInt::from_octets(octets, signum=1)
@@ -213,7 +216,7 @@ pub fn BigInt::from_octets(octets : BytesView, signum? : Int = 1) -> BigInt {
return 0N
}
if octets.is_empty() {
- abort("empty octet string")
+ return 0N
}
let str = StringBuilder()
for octet in octets {
@@ -239,14 +242,21 @@ pub fn BigInt::from_octets(octets : BytesView, signum? : Int = 1) -> BigInt {
/// Returns a byte sequence representing the number in big-endian order.
///
/// Throws a panic if the input number is negative, or if `length` is zero or
-/// negative for a non-zero input.
+/// negative.
pub fn BigInt::to_octets(self : BigInt, length? : Int) -> Bytes {
if self < 0 {
abort("negative BigInt")
}
if self == 0 {
return match length {
- Some(len) => Bytes::make(len, 0)
+ Some(len) =>
+ // keep the documented contract (and the non-js behavior): a
+ // non-positive requested length panics even for zero
+ if len <= 0 {
+ abort("negative length")
+ } else {
+ Bytes::make(len, 0)
+ }
None => [0]
}
}
@@ -651,7 +661,7 @@ pub fn BigInt::to_uint64(self : BigInt) -> UInt64 {
///
/// Parameters:
///
-/// * `value` : The `BigInt` value to be converted.
+/// * `self` : The `BigInt` value to be converted.
///
/// Returns a 64-bit signed integer (`Int64`) representing the lower 64 bits of
/// the input `BigInt`.
diff --git a/bigint/bigint_test.mbt b/bigint/bigint_test.mbt
index adea8ac9ad..5365ef0699 100644
--- a/bigint/bigint_test.mbt
+++ b/bigint/bigint_test.mbt
@@ -66,6 +66,13 @@ test "to_octets pads larger length" {
inspect(n.to_octets(length=5), content="b\"\\x00\\x00\\x01\\x02\\x03\"")
}
+///|
+test "from_octets treats empty input as zero" {
+ inspect(@bigint.BigInt::from_octets(b""), content="0")
+ inspect(@bigint.BigInt::from_octets(b"", signum=0), content="0")
+ inspect(@bigint.BigInt::from_octets(b"", signum=-1), content="0")
+}
+
///|
test "add" {
let a = @bigint.BigInt::from_int64(123456789012345678L)
@@ -1386,3 +1393,31 @@ test "BigInt modpow negative base normalization" {
// (-3)^3 = -27, -27 mod 10 = 3 (canonical non-negative)
inspect((-3N).pow(3N, modulus=10N), content="3")
}
+
+///|
+test "to_string non-power-of-two radixes round-trip and match known values" {
+ // known small values
+ inspect(255N.to_string(radix=3), content="100110")
+ inspect((-255N).to_string(radix=3), content="-100110")
+ inspect(35N.to_string(radix=36), content="z")
+ inspect(36N.to_string(radix=36), content="10")
+ inspect(6N.to_string(radix=6), content="10")
+ // round-trip large values through every non-power-of-two radix
+ let big = (@bigint.BigInt::from_string("123456789123456789") << 700) +
+ @bigint.BigInt::from_string("987654321987654321")
+ for radix in [3, 5, 6, 7, 11, 12, 15, 20, 33, 36] {
+ let s = big.to_string(radix~)
+ assert_eq(@bigint.BigInt::from_string(s, radix~), big)
+ let s_neg = (-big).to_string(radix~)
+ assert_eq(@bigint.BigInt::from_string(s_neg, radix~), -big)
+ }
+ // boundary around a chunk: radix 3 uses 19-digit chunks; exercise values
+ // spanning one and several slots
+ for e in [1, 18, 19, 20, 37, 38, 39] {
+ let p = @bigint.BigInt::from_string("3").pow(
+ @bigint.BigInt::from_string(e.to_string()),
+ )
+ assert_eq(@bigint.BigInt::from_string(p.to_string(radix=3), radix=3), p)
+ inspect(p.to_string(radix=3).length(), content=(e + 1).to_string())
+ }
+}
diff --git a/bigint/bigint_wbtest.mbt b/bigint/bigint_wbtest.mbt
index 7b7b5b8b2d..41007f4fe7 100644
--- a/bigint/bigint_wbtest.mbt
+++ b/bigint/bigint_wbtest.mbt
@@ -575,6 +575,30 @@ test "from_octets" {
check_invariant(a)
@test.assert_eq(a, 0)
+ // Test multiple leading zero limbs
+ let a = BigInt::from_octets(b"\x00\x00\x00\x00\x00\x00\x00\x00\x26")
+ check_invariant(a)
+ inspect(a.to_string(), content="38")
+ @test.assert_eq(a, 38N)
+ @test.assert_eq(a.compare(38N), 0)
+
+ // Test zero spanning multiple limbs
+ let a = BigInt::from_octets(
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ )
+ check_invariant(a)
+ inspect(a.to_string(), content="0")
+ @test.assert_eq(a, 0N)
+ @test.assert_eq(a.compare(0N), 0)
+ let a = BigInt::from_octets(
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ signum=-1,
+ )
+ check_invariant(a)
+ inspect(a.to_string(), content="0")
+ @test.assert_eq(a, 0N)
+ @test.assert_eq(a.compare(0N), 0)
+
// Test positive number
let a = BigInt::from_octets(b"\x01")
check_invariant(a)
diff --git a/bigint/bigint_wide.mbt b/bigint/bigint_wide.mbt
new file mode 100644
index 0000000000..45a422b436
--- /dev/null
+++ b/bigint/bigint_wide.mbt
@@ -0,0 +1,1197 @@
+// 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.
+
+// A sign-magnitude arbitrary-precision integer over 64-bit limbs.
+//
+// Native and wasm1 use `FixedArray[UInt64]` limbs with base 2^64. wasm-gc
+// retains 32-bit limbs, and JavaScript keeps its host-BigInt implementation.
+//
+// Invariants:
+// - len > 0
+// - (exists 0 <= i < len. limbs[i] > 0) => limbs[len-1] > 0
+// - (forall 0 <= i < len. limbs[i] == 0) => limbs[0] == 0 and len == 1
+// - forall len <= i < limbs.length(). limbs[i] == 0
+
+///|
+#valtype
+struct BigInt {
+ limbs : FixedArray[UInt64]
+ sign : Sign
+ len : Int
+}
+
+///|
+priv enum Sign {
+ Positive
+ Negative
+} derive(Eq)
+
+///|
+/// Switch from schoolbook to Karatsuba at this many limbs.
+const KARATSUBA_THRESHOLD = 64
+
+///|
+/// Number of bits represented by each wide limb.
+const RADIX_BIT_LEN = 64
+
+///|
+let zero : BigInt = { limbs: FixedArray::make(1, 0UL), sign: Positive, len: 1, }
+
+///|
+let one : BigInt = { limbs: FixedArray::make(1, 1UL), sign: Positive, len: 1, }
+
+// Construction
+
+///|
+fn make(len : Int) -> FixedArray[UInt64] {
+ FixedArray::make(len, 0UL)
+}
+
+///|
+/// Drop leading zero limbs; never returns less than 1.
+fn normalize_len(limbs : FixedArray[UInt64], max_len : Int) -> Int {
+ let mut i = max_len
+ while i > 1 && limbs.unsafe_get(i - 1) == 0 {
+ i -= 1
+ }
+ i
+}
+
+///|
+/// Creates a non-negative `BigInt` from an unsigned 64-bit integer.
+pub fn BigInt::from_uint64(n : UInt64) -> BigInt {
+ { limbs: FixedArray::make(1, n), sign: Positive, len: 1, }
+}
+
+///|
+/// Creates a non-negative `BigInt` from an unsigned 32-bit integer.
+pub fn BigInt::from_uint(n : UInt) -> BigInt {
+ BigInt::from_uint64(n.to_uint64())
+}
+
+///|
+/// Creates a `BigInt` from a signed 64-bit integer.
+pub fn BigInt::from_int64(n : Int64) -> BigInt {
+ if n == 0 {
+ return zero
+ }
+ if n < 0 {
+ // Negating Int64::MIN overflows, so go through the unsigned domain.
+ let mag = 0UL - n.reinterpret_as_uint64()
+ { limbs: FixedArray::make(1, mag), sign: Negative, len: 1, }
+ } else {
+ {
+ limbs: FixedArray::make(1, n.reinterpret_as_uint64()),
+ sign: Positive,
+ len: 1,
+ }
+ }
+}
+
+///|
+/// Creates a `BigInt` from a signed 32-bit integer.
+pub fn BigInt::from_int(n : Int) -> BigInt {
+ BigInt::from_int64(n.to_int64())
+}
+
+///|
+/// Returns whether this integer is zero.
+pub fn BigInt::is_zero(self : BigInt) -> Bool {
+ self.len == 1 && self.limbs.unsafe_get(0) == 0
+}
+
+///|
+fn BigInt::with_sign(self : BigInt, sign : Sign) -> BigInt {
+ if self.is_zero() {
+ self
+ } else {
+ { ..self, sign, }
+ }
+}
+
+///|
+pub impl Neg for BigInt with fn neg(self : BigInt) -> BigInt {
+ if self.is_zero() {
+ return self
+ }
+ { ..self, sign: if self.sign == Positive { Negative } else { Positive }, }
+}
+
+// Magnitude comparison
+
+///|
+/// Compare |self| with |other|.
+fn BigInt::cmp_mag(self : BigInt, other : BigInt) -> Int {
+ if self.len != other.len {
+ return if self.len < other.len { -1 } else { 1 }
+ }
+ for i = self.len - 1; i >= 0; i = i - 1 {
+ let a = self.limbs.unsafe_get(i)
+ let b = other.limbs.unsafe_get(i)
+ if a != b {
+ return if a < b { -1 } else { 1 }
+ }
+ }
+ 0
+}
+
+///|
+pub impl Compare for BigInt with fn compare(self : BigInt, other : BigInt) -> Int {
+ match (self.sign, other.sign) {
+ (Positive, Negative) =>
+ if self.is_zero() && other.is_zero() {
+ 0
+ } else {
+ 1
+ }
+ (Negative, Positive) =>
+ if self.is_zero() && other.is_zero() {
+ 0
+ } else {
+ -1
+ }
+ (Positive, Positive) => self.cmp_mag(other)
+ (Negative, Negative) => -self.cmp_mag(other)
+ }
+}
+
+///|
+pub impl Eq for BigInt with fn equal(self : BigInt, other : BigInt) -> Bool {
+ self.compare(other) == 0
+}
+
+// Magnitude add / sub
+//
+// These ignore signs entirely; the signed `Add`/`Sub` impls dispatch to them.
+
+///|
+/// |self| + |other|, always positive.
+fn BigInt::add_mag(self : BigInt, other : BigInt) -> BigInt {
+ // Ensure self is the longer operand.
+ let (a, b) = if self.len >= other.len { (self, other) } else { (other, self) }
+ let an = a.len
+ let bn = b.len
+ let limbs = make(an + 1)
+ let c = add_vv(limbs, 0, a.limbs, 0, b.limbs, 0, bn)
+ let c = add_vw(limbs, bn, a.limbs, bn, c, an - bn)
+ limbs.unsafe_set(an, c)
+ { limbs, sign: Positive, len: if c != 0 { an + 1 } else { an }, }
+}
+
+///|
+/// |self| - |other|, requires |self| >= |other|. Always positive.
+fn BigInt::sub_mag(self : BigInt, other : BigInt) -> BigInt {
+ let an = self.len
+ let bn = other.len
+ let limbs = make(an)
+ let c = sub_vv(limbs, 0, self.limbs, 0, other.limbs, 0, bn)
+ ignore(sub_vw(limbs, bn, self.limbs, bn, c, an - bn))
+ { limbs, sign: Positive, len: normalize_len(limbs, an), }
+}
+
+///|
+pub impl Add for BigInt with fn add(self : BigInt, other : BigInt) -> BigInt {
+ if self.sign == other.sign {
+ self.add_mag(other).with_sign(self.sign)
+ } else if self.cmp_mag(other) >= 0 {
+ self.sub_mag(other).with_sign(self.sign)
+ } else {
+ other.sub_mag(self).with_sign(other.sign)
+ }
+}
+
+///|
+pub impl Sub for BigInt with fn sub(self : BigInt, other : BigInt) -> BigInt {
+ if self.sign != other.sign {
+ self.add_mag(other).with_sign(self.sign)
+ } else if self.cmp_mag(other) >= 0 {
+ self.sub_mag(other).with_sign(self.sign)
+ } else {
+ other
+ .sub_mag(self)
+ .with_sign(if self.sign == Positive { Negative } else { Positive })
+ }
+}
+
+// Multiplication
+
+///|
+pub impl Mul for BigInt with fn mul(self : BigInt, other : BigInt) -> BigInt {
+ if self.is_zero() || other.is_zero() {
+ return zero
+ }
+ let sign = if self.sign == other.sign { Positive } else { Negative }
+ // Dispatch on the shorter operand, with the longer operand first.
+ let (a, b) = if self.len >= other.len { (self, other) } else { (other, self) }
+ let ret = if b.len == 1 {
+ a.mul_single_limb(b.limbs.unsafe_get(0))
+ } else if b.len < KARATSUBA_THRESHOLD {
+ a.grade_school_mul(b)
+ } else {
+ a.karatsuba_mul(b)
+ }
+ { ..ret, sign, }
+}
+
+///|
+/// |self| * x for a single limb x. Requires x != 0 and self != 0.
+fn BigInt::mul_single_limb(self : BigInt, x : UInt64) -> BigInt {
+ let n = self.len
+ let limbs = make(n + 1)
+ let c = mul_add_vww(limbs, 0, self.limbs, 0, x, 0, n)
+ limbs.unsafe_set(n, c)
+ { limbs, sign: Positive, len: if c != 0 { n + 1 } else { n }, }
+}
+
+///|
+/// Schoolbook O(n*m) multiply of the magnitudes.
+fn BigInt::grade_school_mul(self : BigInt, other : BigInt) -> BigInt {
+ let an = self.len
+ let bn = other.len
+ let limbs = make(an + bn)
+ basic_mul(limbs, 0, self.limbs, 0, an, other.limbs, 0, bn)
+ { limbs, sign: Positive, len: normalize_len(limbs, an + bn), }
+}
+
+///|
+/// Karatsuba over one scratch buffer.
+///
+/// Requires `self.len >= other.len >= KARATSUBA_THRESHOLD`. Karatsuba itself
+/// only handles the low `k` limbs of each operand (`k` from `karatsuba_len`, so
+/// the recursion splits evenly); whatever sits above `k` is folded back in
+/// afterwards as three ordinary products.
+fn BigInt::karatsuba_mul(self : BigInt, other : BigInt) -> BigInt {
+ let m = self.len
+ let n = other.len
+ let k = karatsuba_len(n, KARATSUBA_THRESHOLD)
+ let scratch = make(6 * k)
+ karatsuba(scratch, 0, self.limbs, 0, other.limbs, 0, k)
+ let z = make(m + n)
+ z.unsafe_blit(0, scratch, 0, 2 * k)
+ if k < n || m != n {
+ // With B = 2^(64k), x = xh*B + x0 and y = yh*B + y0. The scratch pass
+ // produced x0*y0; add x0*yh*B, xh*y0*B and xh*yh*B^2.
+ let x0 = slice_mag(self.limbs, 0, k)
+ let xh = slice_mag(self.limbs, k, m - k)
+ let y0 = slice_mag(other.limbs, 0, k)
+ let yh = slice_mag(other.limbs, k, n - k)
+ add_term(z, m + n, x0 * yh, k)
+ add_term(z, m + n, xh * y0, k)
+ add_term(z, m + n, xh * yh, 2 * k)
+ }
+ { limbs: z, sign: Positive, len: normalize_len(z, m + n), }
+}
+
+///|
+/// `z[i..zn) += t`, skipping the no-op case so `add_at` never walks a zero.
+fn add_term(z : FixedArray[UInt64], zn : Int, t : BigInt, i : Int) -> Unit {
+ if !t.is_zero() {
+ add_at(z, zn, t.limbs, t.len, i)
+ }
+}
+
+///|
+/// A normalized magnitude holding `x[off..off+n)`; `zero` when `n <= 0`.
+fn slice_mag(x : FixedArray[UInt64], off : Int, n : Int) -> BigInt {
+ if n <= 0 {
+ return zero
+ }
+ let mut len = n
+ while len > 1 && x.unsafe_get(off + len - 1) == 0 {
+ len -= 1
+ }
+ let limbs = make(len)
+ limbs.unsafe_blit(0, x, off, len)
+ { limbs, sign: Positive, len, }
+}
+
+// Division
+
+///|
+pub impl Div for BigInt with fn div(self : BigInt, other : BigInt) -> BigInt {
+ let (q, _) = self.div_mod(other)
+ q
+}
+
+///|
+pub impl Mod for BigInt with fn mod(self : BigInt, other : BigInt) -> BigInt {
+ let (_, r) = self.div_mod(other)
+ r
+}
+
+///|
+/// Truncating division: the quotient rounds toward zero and the remainder takes
+/// the sign of the dividend, matching core's `Div`/`Mod`.
+fn BigInt::div_mod(self : BigInt, other : BigInt) -> (BigInt, BigInt) {
+ if other.is_zero() {
+ abort("division by zero")
+ }
+ let cmp = self.cmp_mag(other)
+ if cmp < 0 {
+ return (zero, self)
+ }
+ let q_sign = if self.sign == other.sign { Positive } else { Negative }
+ if cmp == 0 {
+ return (one.with_sign(q_sign), zero)
+ }
+ let (q, r) = if other.len == 1 {
+ self.div_mod_single_limb(other.limbs.unsafe_get(0))
+ } else {
+ self.knuth_div(other)
+ }
+ (q.with_sign(q_sign), r.with_sign(self.sign))
+}
+
+///|
+/// |self| divmod a single limb, using the Möller-Granlund 2/1 divider so the
+/// inner loop is two wide multiplies rather than a hardware 128/64 divide.
+fn BigInt::div_mod_single_limb(self : BigInt, d : UInt64) -> (BigInt, BigInt) {
+ let n = self.len
+ let q = make(n)
+ if d == 1 {
+ q.unsafe_blit(0, self.limbs, 0, n)
+ return ({ limbs: q, sign: Positive, len: n, }, zero)
+ }
+ let s = nlz(d)
+ let dn = d << s
+ let r = div_w(q, self.limbs, n, dn, reciprocal_word(dn), s)
+ (
+ { limbs: q, sign: Positive, len: normalize_len(q, n), },
+ { limbs: FixedArray::make(1, r), sign: Positive, len: 1, },
+ )
+}
+
+///|
+/// Knuth TAOCP 4.3.1 Algorithm D over 64-bit limbs.
+///
+/// Requires `|self| > |other|` and `other.len >= 2`.
+fn BigInt::knuth_div(self : BigInt, other : BigInt) -> (BigInt, BigInt) {
+ let n = other.len
+ let m = self.len - n
+
+ // D1. Normalize so the divisor's top limb has its high bit set.
+ let s = nlz(other.limbs.unsafe_get(n - 1))
+ let v = make(n)
+ if s == 0 {
+ v.unsafe_blit(0, other.limbs, 0, n)
+ } else {
+ ignore(shl_vu(v, other.limbs, s, n))
+ }
+ // u gets one extra limb to hold the shifted-out bits.
+ let u = make(self.len + 1)
+ if s == 0 {
+ u.unsafe_blit(0, self.limbs, 0, self.len)
+ } else {
+ let carry = shl_vu(u, self.limbs, s, self.len)
+ u.unsafe_set(self.len, carry)
+ }
+ let q = make(m + 1)
+ let vn1 = v.unsafe_get(n - 1)
+ let vn2 = v.unsafe_get(n - 2)
+ let rec = reciprocal_word(vn1)
+ let qhatv = make(n + 1)
+ for j = m; j >= 0; j = j - 1 {
+ // D3. Estimate q̂ from the top two limbs.
+ let ujn = u.unsafe_get(j + n)
+ let mut qhat = 0xffff_ffff_ffff_ffffUL
+ if ujn != vn1 {
+ let dm = div2by1(ujn, u.unsafe_get(j + n - 1), vn1, rec)
+ qhat = dm.q
+ let mut rhat = dm.r
+ // Refine: while q̂ * v[n-2] > (r̂ << 64) + u[j+n-2], decrement q̂.
+ let mut p = umul_wide(qhat, vn2)
+ let ujn2 = u.unsafe_get(j + n - 2)
+ while greater_than(p.hi, p.lo, rhat, ujn2) {
+ qhat -= 1
+ let prev = rhat
+ rhat += vn1
+ if rhat < prev {
+ break
+ }
+ p = umul_wide(qhat, vn2)
+ }
+ }
+
+ // D4. Multiply and subtract.
+ let c = mul_add_vww(qhatv, 0, v, 0, qhat, 0, n)
+ qhatv.unsafe_set(n, c)
+ let borrow = sub_vv(u, j, u, j, qhatv, 0, n + 1)
+
+ // D5/D6. Rare: q̂ was one too large, add the divisor back.
+ if borrow != 0 {
+ let carry = add_vv(u, j, u, j, v, 0, n)
+ u.unsafe_set(j + n, u.unsafe_get(j + n) + carry)
+ qhat -= 1
+ }
+ q.unsafe_set(j, qhat)
+ }
+
+ // D8. Unnormalize the remainder.
+ let r = make(n)
+ if s == 0 {
+ r.unsafe_blit(0, u, 0, n)
+ } else {
+ ignore(shr_vu(r, u, s, n))
+ }
+ (
+ { limbs: q, sign: Positive, len: normalize_len(q, m + 1), },
+ { limbs: r, sign: Positive, len: normalize_len(r, n), },
+ )
+}
+
+// Shifts
+
+///|
+pub impl Shl for BigInt with fn shl(self : BigInt, n : Int) -> BigInt {
+ if n < 0 {
+ abort("negative shift count")
+ }
+ if self.is_zero() || n == 0 {
+ return self
+ }
+ let words = n / 64
+ let bits = n % 64
+ let len = self.len + words + 1
+ let limbs = make(len)
+ if bits == 0 {
+ limbs.unsafe_blit(words, self.limbs, 0, self.len)
+ } else {
+ let shifted = make(self.len)
+ let carry = shl_vu(shifted, self.limbs, bits, self.len)
+ limbs.unsafe_blit(words, shifted, 0, self.len)
+ limbs.unsafe_set(words + self.len, carry)
+ }
+ { limbs, sign: self.sign, len: normalize_len(limbs, len), }
+}
+
+///|
+pub impl Shr for BigInt with fn shr(self : BigInt, n : Int) -> BigInt {
+ if n < 0 {
+ abort("negative shift count")
+ }
+ if self.is_zero() || n == 0 {
+ return self
+ }
+ let words = n / 64
+ let bits = n % 64
+ if words >= self.len {
+ // Arithmetic shift: negatives round toward -infinity, like core.
+ return if self.sign == Positive { zero } else { -one }
+ }
+ let len = self.len - words
+ let limbs = make(len)
+ if bits == 0 {
+ limbs.unsafe_blit(0, self.limbs, words, len)
+ } else {
+ let src = make(len)
+ src.unsafe_blit(0, self.limbs, words, len)
+ ignore(shr_vu(limbs, src, bits, len))
+ }
+ let res = { limbs, sign: self.sign, len: normalize_len(limbs, len), }
+ if self.sign == Negative {
+ // Check whether any bit was shifted out; if so round away from zero.
+ let lost = for i in 0.. 0 && (self.limbs.unsafe_get(words) & ((1UL << bits) - 1)) != 0
+ }
+ if lost {
+ return res - one
+ }
+ }
+ res
+}
+
+// Conversions
+
+///|
+/// Returns the number of bits in the minimal representation excluding its
+/// sign bit.
+pub fn BigInt::bit_length(self : BigInt) -> Int {
+ if self.is_zero() {
+ return 0
+ }
+ let mut bits = self.len * RADIX_BIT_LEN -
+ nlz(self.limbs.unsafe_get(self.len - 1))
+ if self.sign == Negative {
+ // Core defines negative bit length from the minimal two's-complement
+ // representation, so -2^k needs one fewer magnitude bit.
+ let is_power_of_two = for i in 0.. 1 {
+ break false
+ }
+ continue one_bits
+ } nobreak {
+ true
+ }
+ if is_power_of_two {
+ bits -= 1
+ }
+ }
+ bits
+}
+
+///|
+/// Largest power of ten that fits in a limb: 10^19 < 2^64.
+const DECIMAL_CHUNK : UInt64 = 10_000_000_000_000_000_000UL
+
+///|
+const DECIMAL_CHUNK_DIGITS = 19
+
+///|
+/// `DECIMAL_CHUNK` already has its top bit set, so it is normalized with a
+/// shift of zero. Cache its expensive software reciprocal at module startup.
+let decimal_chunk_reciprocal : UInt64 = reciprocal_word(DECIMAL_CHUNK)
+
+///|
+/// Decimal rendering by repeated division by 10^19, so each division peels off
+/// 19 digits at a time.
+fn BigInt::to_string_dec(self : BigInt) -> String {
+ if self.is_zero() {
+ return "0"
+ }
+ let n = self.len
+ let work = make(n)
+ work.unsafe_blit(0, self.limbs, 0, n)
+ let mut len = n
+ let chunks = []
+ while len > 1 || work.unsafe_get(0) != 0 {
+ chunks.push(
+ div_w(work, work, len, DECIMAL_CHUNK, decimal_chunk_reciprocal, 0),
+ )
+ len = normalize_len(work, len)
+ }
+ let buf = StringBuilder()
+ if self.sign == Negative {
+ buf.write_char('-')
+ }
+ buf.write_string(chunks[chunks.length() - 1].to_string())
+ for i = chunks.length() - 2; i >= 0; i = i - 1 {
+ let s = chunks[i].to_string()
+ for _ in 0..<(DECIMAL_CHUNK_DIGITS - s.length()) {
+ buf.write_char('0')
+ }
+ buf.write_string(s)
+ }
+ buf.to_string()
+}
+
+///|
+/// `self ^ exp` by square-and-multiply.
+pub fn BigInt::pow(self : BigInt, exp : BigInt, modulus? : BigInt) -> BigInt {
+ if exp.sign == Negative {
+ abort("negative exponent")
+ }
+ match modulus {
+ None => {
+ let bits = exp.bit_length()
+ for i in 0.. {
+ if m.is_zero() || m.sign == Negative {
+ abort("modulus must be positive")
+ }
+ if m.len == 1 && m.limbs.unsafe_get(0) == 1 {
+ return zero
+ }
+ if exp.is_zero() {
+ return one
+ }
+ // Reduce the base into [0, m), matching core's `(self % m + m) % m`.
+ let r = self % m
+ let base = if r.sign == Negative { r + m } else { r }
+ if base.is_zero() {
+ return zero
+ }
+ if (m.limbs.unsafe_get(0) & 1) == 1 {
+ base.pow_mont(exp, m)
+ } else {
+ base.pow_mod_plain(exp, m)
+ }
+ }
+ }
+}
+
+///|
+/// Bit `i` of the magnitude.
+fn BigInt::test_bit(self : BigInt, i : Int) -> Bool {
+ let w = i / 64
+ if w >= self.len {
+ return false
+ }
+ ((self.limbs.unsafe_get(w) >> (i % 64)) & 1) == 1
+}
+
+///|
+/// Square-and-multiply with an explicit reduction each step. Used when the
+/// modulus is even, where Montgomery does not apply (it needs `m` invertible
+/// mod 2^64).
+fn BigInt::pow_mod_plain(self : BigInt, exp : BigInt, m : BigInt) -> BigInt {
+ let bits = exp.bit_length()
+ for i in 0.. 0`. The win over `pow_mod_plain` is
+/// that every reduction becomes a Montgomery multiply — O(n^2) shift-and-add
+/// instead of a full Knuth D division — and the window cuts the number of
+/// multiplies by roughly a third.
+fn BigInt::pow_mont(self : BigInt, exp : BigInt, m : BigInt) -> BigInt {
+ let n = m.len
+ let mlimbs = m.limbs
+ let k0 = mont_k0(mlimbs.unsafe_get(0))
+ let t = make(2 * n)
+
+ // x, one, and RR = 2^(128n) mod m, each padded to exactly n limbs.
+ let x = make(n)
+ x.unsafe_blit(0, self.limbs, 0, self.len)
+ let one_arr = make(n)
+ one_arr.unsafe_set(0, 1)
+ let rr_big = (one << (2 * 64 * n)) % m
+ let rr = make(n)
+ rr.unsafe_blit(0, rr_big.limbs, 0, rr_big.len)
+
+ // powers[i] = Montgomery form of self^i, for the 4-bit window.
+ let powers : Array[FixedArray[UInt64]] = []
+ for _ in 0..<16 {
+ powers.push(make(n))
+ }
+ montgomery(powers[0], one_arr, rr, mlimbs, k0, n, t)
+ montgomery(powers[1], x, rr, mlimbs, k0, n, t)
+ for i in 2..<16 {
+ montgomery(powers[i], powers[i - 1], powers[1], mlimbs, k0, n, t)
+ }
+ let mut z = make(n)
+ z.unsafe_blit(0, powers[0], 0, n)
+ let mut zz = make(n)
+ for i = exp.len - 1; i >= 0; i = i - 1 {
+ let mut yi = exp.limbs.unsafe_get(i)
+ let mut j = 0
+ while j < 64 {
+ // Four squarings per nibble, skipped only on the very first window.
+ if i != exp.len - 1 || j != 0 {
+ montgomery(zz, z, z, mlimbs, k0, n, t)
+ montgomery(z, zz, zz, mlimbs, k0, n, t)
+ montgomery(zz, z, z, mlimbs, k0, n, t)
+ montgomery(z, zz, zz, mlimbs, k0, n, t)
+ }
+ montgomery(zz, z, powers[(yi >> 60).to_int()], mlimbs, k0, n, t)
+ let swap = z
+ z = zz
+ zz = swap
+ yi = yi << 4
+ j += 4
+ }
+ }
+ // Leave Montgomery form.
+ montgomery(zz, z, one_arr, mlimbs, k0, n, t)
+ let res = { limbs: zz, sign: Positive, len: normalize_len(zz, n), }
+ // Montgomery's conditional subtraction leaves the result below 2m, not m.
+ if res.cmp_mag(m) >= 0 {
+ res.sub_mag(m)
+ } else {
+ res
+ }
+}
+
+// Conversions and bitwise operations.
+
+///|
+fn minimum_int(a : Int, b : Int) -> Int {
+ if a < b {
+ a
+ } else {
+ b
+ }
+}
+
+///|
+fn maximum_int(a : Int, b : Int) -> Int {
+ if a > b {
+ a
+ } else {
+ b
+ }
+}
+
+///|
+fn digit_from_char(x : Int) -> Int {
+ match x {
+ '0'..='9' => x - '0'
+ 'A'..='Z' => x + (10 - 'A')
+ 'a'..='z' => x + (10 - 'a')
+ _ => -1
+ }
+}
+
+///|
+fn char_from_digit(d : Int) -> Char {
+ if d < 10 {
+ (d + '0').unsafe_to_char()
+ } else {
+ (d - 10 + 'a').unsafe_to_char()
+ }
+}
+
+///|
+fn pow2_shift(radix : Int) -> Int? {
+ if radix >= 2 && (radix & (radix - 1)) == 0 {
+ Some(radix.ctz())
+ } else {
+ None
+ }
+}
+
+///|
+/// Converts this integer to a string in a radix between 2 and 36.
+pub fn BigInt::to_string(self : BigInt, radix? : Int = 10) -> String {
+ if radix < 2 || radix > 36 {
+ abort("radix must be between 2 and 36")
+ }
+ if radix == 10 {
+ self.to_string_dec()
+ } else {
+ self.to_string_radix(radix)
+ }
+}
+
+///|
+fn BigInt::to_string_radix(self : BigInt, radix : Int) -> String {
+ if self.is_zero() {
+ return "0"
+ }
+ match pow2_shift(radix) {
+ Some(shift) => self.to_string_radix_pow2(shift)
+ None => {
+ let is_negative = self.sign == Negative
+ let base = BigInt::from_int(radix)
+ let mut value = if is_negative { -self } else { self }
+ let digits = []
+ while !value.is_zero() {
+ let (q, r) = value.div_mod(base)
+ digits.push(char_from_digit(r.to_int()))
+ value = q
+ }
+ let builder = StringBuilder(
+ size_hint=digits.length() + (if is_negative { 1 } else { 0 }),
+ )
+ if is_negative {
+ builder.write_char('-')
+ }
+ for i in digits.length()>..0 {
+ builder.write_char(digits[i])
+ }
+ builder.to_string()
+ }
+ }
+}
+
+///|
+fn BigInt::to_string_radix_pow2(self : BigInt, shift : Int) -> String {
+ let is_negative = self.sign == Negative
+ let value = if is_negative { -self } else { self }
+ let bit_len = value.bit_length()
+ let digit_len = (bit_len + shift - 1) / shift
+ let builder = StringBuilder(
+ size_hint=digit_len + (if is_negative { 1 } else { 0 }),
+ )
+ if is_negative {
+ builder.write_char('-')
+ }
+ let mask = (1UL << shift) - 1
+ for pos in digit_len>..0 {
+ let bit_index = pos * shift
+ let limb_index = bit_index / RADIX_BIT_LEN
+ let offset = bit_index % RADIX_BIT_LEN
+ let mut chunk = value.limbs.unsafe_get(limb_index) >> offset
+ if offset + shift > RADIX_BIT_LEN && limb_index + 1 < value.len {
+ chunk = chunk |
+ (value.limbs.unsafe_get(limb_index + 1) << (RADIX_BIT_LEN - offset))
+ }
+ builder.write_char(char_from_digit((chunk & mask).to_int()))
+ }
+ builder.to_string()
+}
+
+///|
+fn BigInt::from_string_radix(input : StringView, radix : Int) -> BigInt raise {
+ match pow2_shift(radix) {
+ Some(shift) => BigInt::from_string_radix_pow2(input, radix, shift)
+ None => {
+ let len = input.length()
+ if len == 0 {
+ syntax_err()
+ }
+ let sign = if input.unsafe_get(0) == '-' { Negative } else { Positive }
+ let start = if sign == Negative { 1 } else { 0 }
+ if start == len {
+ syntax_err()
+ }
+ let base = BigInt::from_int(radix)
+ let acc = for i in start..= radix {
+ syntax_err()
+ }
+ continue acc * base + BigInt::from_int(digit)
+ } nobreak {
+ acc
+ }
+ acc.with_sign(sign)
+ }
+ }
+}
+
+///|
+fn BigInt::from_string_radix_pow2(
+ input : StringView,
+ radix : Int,
+ shift : Int,
+) -> BigInt raise {
+ let len = input.length()
+ if len == 0 {
+ syntax_err()
+ }
+ let sign = if input.unsafe_get(0) == '-' { Negative } else { Positive }
+ let start = if sign == Negative { 1 } else { 0 }
+ if start == len {
+ syntax_err()
+ }
+ let mut first = start
+ while first < len {
+ let digit = digit_from_char(input.unsafe_get(first).to_int())
+ if digit < 0 || digit >= radix {
+ syntax_err()
+ }
+ if digit != 0 {
+ break
+ }
+ first += 1
+ }
+ if first == len {
+ return zero
+ }
+ let total_bits = (len - first) * shift
+ let limbs_len = (total_bits + RADIX_BIT_LEN - 1) / RADIX_BIT_LEN
+ let limbs = FixedArray::make(limbs_len, 0UL)
+ for i in len>..first; bit_pos = 0 {
+ let digit = digit_from_char(input.unsafe_get(i).to_int())
+ if digit < 0 || digit >= radix {
+ syntax_err()
+ }
+ let limb_index = bit_pos / RADIX_BIT_LEN
+ let offset = bit_pos % RADIX_BIT_LEN
+ let value = digit.to_uint64()
+ limbs.unsafe_set(
+ limb_index,
+ limbs.unsafe_get(limb_index) | (value << offset),
+ )
+ if offset + shift > RADIX_BIT_LEN {
+ let hi = value >> (RADIX_BIT_LEN - offset)
+ limbs.unsafe_set(limb_index + 1, limbs.unsafe_get(limb_index + 1) | hi)
+ }
+ continue bit_pos + shift
+ }
+ { limbs, sign, len: normalize_len(limbs, limbs_len), }
+}
+
+///|
+fn BigInt::from_string_dec(input : StringView) -> BigInt raise {
+ let len = input.length()
+ if len == 0 {
+ syntax_err()
+ }
+ let sign = if input.unsafe_get(0) == '-' { Negative } else { Positive }
+ let mut i = if sign == Negative { 1 } else { 0 }
+ if i == len {
+ syntax_err()
+ }
+ let mut acc = zero
+ while i < len {
+ let take = minimum_int(DECIMAL_CHUNK_DIGITS, len - i)
+ let (chunk, scale) = for offset in 0.. 9 {
+ syntax_err()
+ }
+ continue chunk * 10 + digit.to_uint64(), scale * 10
+ } nobreak {
+ (chunk, scale)
+ }
+ i += take
+ acc = acc * BigInt::from_uint64(scale) + BigInt::from_uint64(chunk)
+ }
+ acc.with_sign(sign)
+}
+
+///|
+/// Parses a string into a `BigInt` using a base between 2 and 36.
+#internal(internal, "use `@string.parse_bigint` instead")
+#doc(hidden)
+pub fn parse_bigint(str : StringView, base? : Int = 10) -> BigInt raise {
+ if base < 2 || base > 36 {
+ base_err()
+ }
+ if base == 10 {
+ BigInt::from_string_dec(str)
+ } else {
+ BigInt::from_string_radix(str, base)
+ }
+}
+
+///|
+/// Converts a string representation in the specified radix to a `BigInt`.
+///
+/// Panics if the input is malformed. Use `@string.parse_bigint` to handle
+/// errors.
+pub fn BigInt::from_string(input : String, radix? : Int = 10) -> BigInt {
+ parse_bigint(input.view(), base=radix) catch {
+ Failure(msg) => abort(msg)
+ _ => abort("invalid syntax")
+ }
+}
+
+///|
+/// Creates a magnitude from unsigned big-endian bytes.
+pub fn BigInt::from_octets(input : BytesView, signum? : Int = 1) -> BigInt {
+ if signum == 0 || input.length() == 0 {
+ return zero
+ }
+ if signum < 0 {
+ return -BigInt::from_octets(input)
+ }
+ let byte_len = input.length()
+ let full_limbs = byte_len / 8
+ let head_len = byte_len % 8
+ let limbs_len = if head_len == 0 { full_limbs } else { full_limbs + 1 }
+ let limbs = FixedArray::make(limbs_len, 0UL)
+ // Assemble the partial most-significant limb.
+ for i in 0.. Bytes {
+ let minimum_length = match length {
+ None => 1
+ Some(value) => if value <= 0 { abort("negative length") } else { value }
+ }
+ if self.is_zero() {
+ return Bytes::new(maximum_int(1, minimum_length))
+ }
+ if self.sign == Negative {
+ abort("negative BigInt")
+ }
+ let value_length = (self.bit_length() + 7) / 8
+ let result_length = maximum_int(minimum_length, value_length)
+ let result = FixedArray::make(result_length, b'\x00')
+ for i in 0..> (i % 8 * 8)) & 0xffUL).to_int().to_byte(),
+ )
+ }
+ unsafe_fixedarray_to_bytes(result)
+}
+
+///|
+fn fill_twos_complement(value : BigInt, out : FixedArray[UInt64]) -> Unit {
+ out.unsafe_blit(0, value.limbs, 0, value.len)
+ if value.sign == Negative {
+ for i in 0.. UInt64,
+) -> BigInt {
+ let limbs_len = maximum_int(self.len, other.len) + 1
+ let x = FixedArray::make(limbs_len, 0UL)
+ let y = FixedArray::make(limbs_len, 0UL)
+ fill_twos_complement(self, x)
+ fill_twos_complement(other, y)
+ for i in 0.. BigInt {
+ self.bitwise(other, (a, b) => a & b)
+}
+
+///|
+pub impl BitOr for BigInt with fn lor(self : BigInt, other : BigInt) -> BigInt {
+ self.bitwise(other, (a, b) => a | b)
+}
+
+///|
+pub impl BitXOr for BigInt with fn lxor(self : BigInt, other : BigInt) -> BigInt {
+ self.bitwise(other, (a, b) => a ^ b)
+}
+
+///|
+/// Returns the low 32 bits reinterpreted as a signed integer.
+pub fn BigInt::to_int(self : BigInt) -> Int {
+ self.to_uint().reinterpret_as_int()
+}
+
+///|
+/// Returns the low 32 bits as an unsigned integer.
+pub fn BigInt::to_uint(self : BigInt) -> UInt {
+ let low = self.limbs.unsafe_get(0).to_uint()
+ if self.sign == Negative {
+ 0U - low
+ } else {
+ low
+ }
+}
+
+///|
+/// Returns the low 64 bits reinterpreted as a signed integer.
+pub fn BigInt::to_int64(self : BigInt) -> Int64 {
+ self.to_uint64().reinterpret_as_int64()
+}
+
+///|
+/// Returns the low 64 bits as an unsigned integer.
+pub fn BigInt::to_uint64(self : BigInt) -> UInt64 {
+ let low = self.limbs.unsafe_get(0)
+ if self.sign == Negative {
+ 0UL - low
+ } else {
+ low
+ }
+}
+
+///|
+/// Returns the number of trailing zero bits in the magnitude.
+pub fn BigInt::ctz(self : BigInt) -> Int {
+ if self.is_zero() {
+ return 0
+ }
+ let mut i = 0
+ while self.limbs.unsafe_get(i) == 0 {
+ i += 1
+ }
+ RADIX_BIT_LEN * i + self.limbs.unsafe_get(i).ctz()
+}
+
+///|
+fn unsafe_fixedarray_to_bytes(arr : FixedArray[Byte]) -> Bytes = "%identity"
+
+///|
+fn can_convert_to_int(x : BigInt) -> Bool {
+ x.len == 1 &&
+ (if x.sign == Negative {
+ x.limbs.unsafe_get(0) <= 0x8000_0000UL
+ } else {
+ x.limbs.unsafe_get(0) < 0x8000_0000UL
+ })
+}
+
+///|
+fn can_convert_to_int64(x : BigInt) -> Bool {
+ x.len == 1 &&
+ (if x.sign == Negative {
+ x.limbs.unsafe_get(0) <= 0x8000_0000_0000_0000UL
+ } else {
+ x.limbs.unsafe_get(0) < 0x8000_0000_0000_0000UL
+ })
+}
+
+///|
+fn is_neg(x : BigInt) -> Bool {
+ x.sign == Negative
+}
+
+///|
+/// Returns the magnitude as 32-bit words so hashing remains representation
+/// independent and agrees with the JavaScript backend.
+fn BigInt::limbs(self : Self) -> Array[UInt] {
+ let result = []
+ for i in 0..> 32).to_uint()
+ if i + 1 < self.len || high != 0 {
+ result.push(high)
+ }
+ }
+ result
+}
diff --git a/bigint/bigint_wide_wbtest.mbt b/bigint/bigint_wide_wbtest.mbt
new file mode 100644
index 0000000000..4a5e8e44fc
--- /dev/null
+++ b/bigint/bigint_wide_wbtest.mbt
@@ -0,0 +1,207 @@
+// 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.
+
+///|
+priv struct MyBigInt(BigInt)
+
+///|
+impl Show for MyBigInt with fn output(self, logger) {
+ let sign = match self.sign {
+ Positive => "Positive"
+ Negative => "Negative"
+ }
+ logger <+
+ "{limbs : \{@debug.Repr(self.limbs)}, sign : \{sign}, len : \{self.len} }"
+}
+
+///|
+test "debug_string" {
+ let buf = StringBuilder()
+ let v : Array[MyBigInt] = [0, 1, 2, 3, 4, -0, -1, -2, -3]
+ (buf as &Logger).write_iter(v.iter(), sep="\n", prefix="", suffix="")
+ // Logger::writer_iter()
+ // trait logger has no method write_iter
+ // precise:
+ // (dyn Logger)::write_iter(buf,..)
+ // Logger::trait_method()
+ inspect(
+ buf,
+ content=(
+ #|{limbs : , sign : Positive, len : 1 }
+ #|{limbs : , sign : Positive, len : 1 }
+ #|{limbs : , sign : Positive, len : 1 }
+ #|{limbs : , sign : Positive, len : 1 }
+ #|{limbs : , sign : Positive, len : 1 }
+ #|{limbs : , sign : Positive, len : 1 }
+ #|{limbs : , sign : Negative, len : 1 }
+ #|{limbs : , sign : Negative, len : 1 }
+ #|{limbs : , sign : Negative, len : 1 }
+ ),
+ )
+}
+
+///|
+fn check_invariant(a : BigInt) -> Unit raise {
+ guard a.len > 0 else { fail("invariant len > 0 is broken: len = \{a.len}") }
+ if a.limbs.iter().take(a.len).any(x => x > 0) {
+ guard a.limbs[a.len - 1] > 0 else {
+ fail(
+ "invariant (exists 0 <= i < len. limbs[i] > 0) => limbs[len-1] > 0 is broken",
+ )
+ }
+ } else {
+ guard a.len == 1 && a.limbs[0] == 0 else {
+ fail(
+ "invariant (forall 0 <= i < len. limbs[i] == 0) => limbs[0] == 0 and len == 1 is broken: len = \{a.len}, limbs = \{@debug.Repr(a.limbs)}",
+ )
+ }
+ }
+ guard a.limbs.iter().drop(a.len).all(x => x == 0) else {
+ fail(
+ "invariant forall len <= i < limbs.length(). limbs[i] == 0 is broken: len = \{a.len}, limbs = \{@debug.Repr(a.limbs)}",
+ )
+ }
+}
+
+///|
+test "shr" {
+ let a = BigInt::from_int64(1234567890123456789L)
+ let b = a >> 1
+ check_invariant(b)
+ inspect(b, content="617283945061728394")
+ let c = a >> 64
+ check_invariant(c)
+ inspect(c, content="0")
+ // 2^127 spans two UInt64 limbs; shifting by two complete limbs drops it.
+ let a = 1N << 127
+ let b = a >> (RADIX_BIT_LEN * 2)
+ check_invariant(b)
+ inspect(b, content="0")
+}
+
+///|
+test "mul_single_limb boundaries" {
+ // One-limb by one-limb with maximal carry-out: (2^64 - 1)^2.
+ let max_limb = 18446744073709551615N
+ let p = max_limb * max_limb
+ check_invariant(p)
+ @test.assert_eq(p, (1N << 128) - (1N << 65) + 1N)
+ // Three all-max limbs times one max limb; carry grows len to n + 1.
+ let a = (1N << 192) - 1N
+ let expected = (1N << 256) - (1N << 192) - (1N << 64) + 1N
+ let p = a * max_limb
+ check_invariant(p)
+ @test.assert_eq(p, expected)
+ // commuted operand order takes the same fast path
+ let p = max_limb * a
+ check_invariant(p)
+ @test.assert_eq(p, expected)
+ // no carry-out: len stays at self.len, the spare limb beyond len stays zero
+ let p = (1N << 128) * 2N
+ check_invariant(p)
+ @test.assert_eq(p, 1N << 129)
+ // the dispatch applies the sign on top of the magnitude-only helper
+ @test.assert_eq(a * -max_limb, -expected)
+ @test.assert_eq(-a * max_limb, -expected)
+ @test.assert_eq(-a * -max_limb, expected)
+ // fast path agrees with the general routine on the same magnitudes
+ let fast = a.mul_single_limb(0xffff_ffff_ffff_ffffUL)
+ check_invariant(fast)
+ @test.assert_eq(fast, a.grade_school_mul(max_limb))
+}
+
+///|
+test "multiplication around the wide Karatsuba threshold" {
+ let cases : Array[(Int, Int)] = [
+ (63, 63), // schoolbook on both sides of the last pre-threshold case
+ (63, 64), // asymmetric input still dispatches on the shorter operand
+ (64, 64), // first exact Karatsuba case
+ (64, 65), // Karatsuba with one high tail limb
+ (65, 65),
+ ]
+ for case in cases {
+ let (limbs_a, limbs_b) = case
+ let bits_a = limbs_a * RADIX_BIT_LEN
+ let bits_b = limbs_b * RADIX_BIT_LEN
+ let a = (1N << bits_a) - 1N
+ let b = (1N << bits_b) - 1N
+ let expected = (1N << (bits_a + bits_b)) -
+ (1N << bits_a) -
+ (1N << bits_b) +
+ 1N
+ @test.assert_eq(a.len, limbs_a)
+ @test.assert_eq(b.len, limbs_b)
+ let product = a * b
+ check_invariant(product)
+ @test.assert_eq(product, expected)
+ }
+}
+
+///|
+test "add coverage for max(self_len, other_len)" {
+ let a = BigInt::from_int(123456789)
+ let b = BigInt::from_int(987654321)
+ let result = a + b
+ inspect(a.len, content="1")
+ inspect(b.len, content="1")
+ inspect(result.len, content="1")
+}
+
+///|
+test "sub coverage for max(self_len, other_len)" {
+ let a = BigInt::from_int(987654321)
+ let b = BigInt::from_int(123456789)
+ let result = a - b
+ inspect(a.len, content="1")
+ inspect(b.len, content="1")
+ inspect(result.len, content="1")
+}
+
+///|
+test "BigInt::bit_length" {
+ inspect(0N.bit_length(), content="0")
+ inspect(1N.bit_length(), content="1")
+ inspect((-1N).bit_length(), content="0")
+ inspect(16N.bit_length(), content="5")
+ inspect((-16N).bit_length(), content="4")
+ inspect(42N.bit_length(), content="6")
+ inspect((-42N).bit_length(), content="6")
+ inspect(1024N.bit_length(), content="11")
+ inspect(10000000N.bit_length(), content="24")
+ inspect((1N << 31).bit_length(), content="32")
+ inspect((1N << 63).bit_length(), content="64")
+}
+
+///|
+test "BigInt::ctz" {
+ inspect(0N.ctz(), content="0")
+ inspect(1N.ctz(), content="0")
+ inspect(8N.ctz(), content="3") // 1000
+ inspect(12N.ctz(), content="2") // 1100
+ inspect(174056N.ctz(), content="3") // 101010011111101000
+ inspect((1N << 31).ctz(), content="31")
+ inspect((1N << 63).ctz(), content="63")
+}
+
+///|
+test {
+ inspect(
+ MyBigInt(-9223372036854775809N),
+ content="{limbs : , sign : Negative, len : 1 }",
+ ) // Int64.min_value - 1
+ inspect(
+ MyBigInt(-9223372036854775810N),
+ content="{limbs : , sign : Negative, len : 1 }",
+ ) // Int64.min_value - 2
+}
diff --git a/bigint/deprecated.mbt b/bigint/deprecated.mbt
deleted file mode 100644
index 2f37fba359..0000000000
--- a/bigint/deprecated.mbt
+++ /dev/null
@@ -1,129 +0,0 @@
-// 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.
-
-///|
-/// Deprecated: use `to_string(radix=16)` instead.
-#deprecated("Use `to_string(radix=16)` instead")
-#coverage.skip
-#cfg(not(target="js"))
-pub fn BigInt::to_hex(self : BigInt, uppercase? : Bool = true) -> String {
- if self.is_zero() {
- return "0"
- }
- // WARN: this implementation assumes that `RADIX_BIT_LEN` is a multiple of 4.
- let digits_per_limb = RADIX_BIT_LEN / 4
- let buf = if self.sign is Negative {
- let builder = StringBuilder(size_hint=self.len * digits_per_limb + 2)
- builder.write_char('-')
- builder
- } else {
- StringBuilder(size_hint=self.len * digits_per_limb)
- }
- for i in self.len>..0 {
- // split the limb into 4-bit chunks
- let digits = FixedArray::make(digits_per_limb, '0')
- let idx = for x = self.limbs[i], idx = 0; x > 0; {
- let y = x % 16
- digits[idx] = if y < 10 {
- (y.reinterpret_as_int() + '0'.to_int()).unsafe_to_char()
- } else if uppercase {
- (y.reinterpret_as_int() - 10 + 'A'.to_int()).unsafe_to_char()
- } else {
- (y.reinterpret_as_int() - 10 + 'a'.to_int()).unsafe_to_char()
- }
- continue x / 16, idx + 1
- } nobreak {
- idx
- }
- let idx = if i != self.len - 1 { digits_per_limb } else { idx }
- for j in 0.. String =
- #|(x, uppercase) => {
- #| const r = x.toString(16);
- #| return uppercase ? r.toUpperCase() : r;
- #|}
-
-///|
-/// Deprecated: use `from_string(radix=16)` instead.
-#deprecated("Use `from_string(radix=16)` instead")
-#coverage.skip
-#cfg(not(target="js"))
-pub fn BigInt::from_hex(input : String) -> BigInt {
- // WARN: this implementation assumes that `RADIX_BIT_LEN` is a multiple of 4.
- fn char_from_hex(x : Int) -> UInt {
- (match x {
- '0'..='9' => x - '0'
- 'A'..='F' => x + (10 - 'A')
- 'a'..='f' => x + (10 - 'a')
- _ => abort("invalid character")
- }).reinterpret_as_uint()
- }
-
- let len = input.length()
- if len == 0 {
- abort("empty string")
- }
- let (sign, number_len) = if input.unsafe_get(0) == '-' {
- (Negative, len - 1)
- } else {
- (Positive, len)
- }
- let nb_char = RADIX_BIT_LEN / 4 // number of char per limb
- let quotient = number_len / nb_char
- let mod = number_len % nb_char
- let b_len = if mod == 0 { quotient } else { quotient + 1 }
- let b = FixedArray::make(b_len, 0U)
- if mod != 0 {
- let start = len - quotient * nb_char - mod
- for i in 0.. 1; {
- continue b_len - 1
- } nobreak {
- b_len
- }
- let sign = if b_len == 1 && b[0] == 0 { Positive } else { sign }
- { limbs: b, sign, len: b_len }
-}
-
-///|
-/// Deprecated: use `from_string(radix=16)` instead.
-#deprecated("Use `from_string(radix=16)` instead")
-#coverage.skip
-#cfg(target="js")
-pub extern "js" fn BigInt::from_hex(str : String) -> BigInt =
- #|(x) => x.startsWith('-') ? -BigInt(`0x${x.slice(1)}`) : BigInt(`0x${x}`)
diff --git a/strconv/number_test.mbt b/bigint/from_octets_bench_test.mbt
similarity index 55%
rename from strconv/number_test.mbt
rename to bigint/from_octets_bench_test.mbt
index 37f98e938b..47b31ab771 100644
--- a/strconv/number_test.mbt
+++ b/bigint/from_octets_bench_test.mbt
@@ -13,24 +13,18 @@
// limitations under the License.
///|
-#warnings("-deprecated")
-test "parse_inf_nan positive NaN" {
- let result = @strconv.parse_double("+nan")
- inspect(result.is_nan(), content="true")
+fn from_octets_bench_data(len : Int) -> Bytes {
+ Bytes::makei(len, i => ((i * 37 + 11) & 0xff).to_byte())
}
///|
-#warnings("-deprecated")
-test "parse_inf_nan negative NaN" {
- let result = @strconv.parse_double("-nan")
- inspect(result.is_nan(), content="true")
+test "bench BigInt::from_octets n=64" (it : @bench.T) {
+ let data = from_octets_bench_data(64)
+ it.bench(fn() { it.keep(@bigint.BigInt::from_octets(data)) })
}
///|
-#warnings("-deprecated")
-test "from_str generic" {
- let i : Int = @strconv.from_str("123")
- inspect(i, content="123")
- let j : Int64 = @strconv.from_str("1234567890123")
- inspect(j, content="1234567890123")
+test "bench BigInt::from_octets n=1024" (it : @bench.T) {
+ let data = from_octets_bench_data(1024)
+ it.bench(fn() { it.keep(@bigint.BigInt::from_octets(data)) })
}
diff --git a/bigint/moon.pkg b/bigint/moon.pkg
index d3e778f145..43056d1571 100644
--- a/bigint/moon.pkg
+++ b/bigint/moon.pkg
@@ -23,9 +23,12 @@ warnings = "-29"
options(
targets: {
+ "arith_wide.mbt": [ "native", "wasm" ],
"bigint_js.mbt": [ "js" ],
"bigint_js_wbtest.mbt": [ "js" ],
- "bigint_nonjs.mbt": [ "not", "js" ],
- "bigint_nonjs_wbtest.mbt": [ "not", "js" ],
+ "bigint_default.mbt": [ "not", "js", "wasm", "native" ],
+ "bigint_default_wbtest.mbt": [ "not", "js", "wasm", "native" ],
+ "bigint_wide.mbt": [ "native", "wasm" ],
+ "bigint_wide_wbtest.mbt": [ "native", "wasm" ],
},
)
diff --git a/bigint/octets_regression_test.mbt b/bigint/octets_regression_test.mbt
new file mode 100644
index 0000000000..e4f4052f1b
--- /dev/null
+++ b/bigint/octets_regression_test.mbt
@@ -0,0 +1,31 @@
+// 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.
+
+// Regression test for moonbitlang/core#4052 — the js `to_octets` ran its
+// zero shortcut before validating the requested length, silently
+// returning b"" for `0N.to_octets(length=0)` while the documented
+// contract (and every other target) panics on a non-positive length.
+
+///|
+test "panic to_octets of zero rejects non-positive length" {
+ // documented contract: non-positive length panics on every target
+ // (the js implementation used to return b"" here)
+ let _ = 0N.to_octets(length=0)
+}
+
+///|
+test "to_octets of zero still pads positive lengths" {
+ inspect(0N.to_octets(), content="b\"\\x00\"")
+ inspect(0N.to_octets(length=3), content="b\"\\x00\\x00\\x00\"")
+}
diff --git a/bigint/panic_test.mbt b/bigint/panic_test.mbt
index 347fe11ac1..b603121d9c 100644
--- a/bigint/panic_test.mbt
+++ b/bigint/panic_test.mbt
@@ -85,11 +85,6 @@ test "panic pow negative exponent" {
base.pow(exp) |> ignore
}
-///|
-test "panic from_octets empty" {
- @bigint.BigInt::from_octets(Bytes::new(0)) |> ignore
-}
-
///|
test "panic to_octets negative" {
(-1N).to_octets() |> ignore
diff --git a/bigint/pkg.generated.mbti b/bigint/pkg.generated.mbti
index 0541c82e3c..e001d571f2 100644
--- a/bigint/pkg.generated.mbti
+++ b/bigint/pkg.generated.mbti
@@ -26,8 +26,6 @@ pub fn BigInt::equal_int(Self, Int) -> Bool
pub fn BigInt::equal_int64(Self, Int64) -> Bool
pub fn BigInt::equal_uint(Self, UInt) -> Bool
pub fn BigInt::equal_uint64(Self, UInt64) -> Bool
-#deprecated
-pub fn BigInt::from_hex(String) -> Self
pub fn BigInt::from_int(Int) -> Self
pub fn BigInt::from_int64(Int64) -> Self
pub fn BigInt::from_octets(BytesView, signum? : Int) -> Self
@@ -46,8 +44,6 @@ pub fn BigInt::pow(Self, Self, modulus? : Self) -> Self
pub fn BigInt::shl(Self, Int) -> Self
pub fn BigInt::shr(Self, Int) -> Self
pub fn BigInt::sub(Self, Self) -> Self
-#deprecated
-pub fn BigInt::to_hex(Self, uppercase? : Bool) -> String
pub fn BigInt::to_int(Self) -> Int
pub fn BigInt::to_int16(Self) -> Int16
pub fn BigInt::to_int64(Self) -> Int64
diff --git a/bigint/quickcheck_deep_test.mbt b/bigint/quickcheck_deep_test.mbt
new file mode 100644
index 0000000000..37a0f4ed30
--- /dev/null
+++ b/bigint/quickcheck_deep_test.mbt
@@ -0,0 +1,464 @@
+// 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.
+
+// Deep arithmetic properties for BigInt, complementing quickcheck_test.mbt.
+// That suite pins small-value semantics (an Int64 oracle) and
+// construction-path invariance; this one drives the large-operand
+// algorithm paths that only engage far beyond 64 bits:
+//
+// * multiplication straddling the Karatsuba threshold (50 limbs),
+// cross-checked against products reassembled from sub-threshold chunks
+// and against closed forms with maximal carry chains;
+// * Knuth division (TAOCP 4.3.1) with adversarial divisor top limbs,
+// where quotient-digit estimation is famously fragile;
+// * shifts, radix round-trips, pow/modpow, octet round-trips, and
+// fixed-width truncation, at sizes crossing many limb boundaries.
+//
+// All inputs are deterministic functions of literal seeds (SplitMix64),
+// so every target runs the identical value stream — any cross-target
+// divergence is itself a failure. This suite found two such divergences,
+// moonbitlang/core#4050 (non-js `to_uint`/`to_uint64` truncation of
+// large negatives) and moonbitlang/core#4052 (js `to_octets` length
+// validation); their dedicated regression tests live alongside the
+// respective fixes (truncation_regression_test.mbt and
+// octets_regression_test.mbt).
+
+///|
+fn mix64(s : UInt64) -> UInt64 {
+ let z = s * 6364136223846793005UL + 1442695040888963407UL
+ let z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL
+ let z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL
+ z ^ (z >> 31)
+}
+
+///|
+fn seed_u64(i : Int) -> UInt64 {
+ i.to_int64().reinterpret_as_uint64()
+}
+
+///|
+/// A pseudorandom positive BigInt with exactly `limbs` 32-bit limbs (the
+/// top byte is forced nonzero), built through the octet path.
+fn mag_of_limbs(seed : UInt64, limbs : Int) -> @bigint.BigInt {
+ let n = limbs * 4
+ let bytes = Bytes::makei(n, i => {
+ let b = (mix64(seed + seed_u64(i)) & 0xffUL).to_int()
+ let b = if i == 0 && b == 0 { 1 } else { b }
+ b.to_byte()
+ })
+ @bigint.BigInt::from_octets(bytes)
+}
+
+///|
+/// Multiplication reassembled from 1024-bit (32-limb) chunk products.
+/// Every chunk product stays below the Karatsuba threshold, so this is an
+/// independently-routed oracle for the Karatsuba path.
+fn chunked_mul(a : @bigint.BigInt, b : @bigint.BigInt) -> @bigint.BigInt {
+ let chunk_bits = 1024
+ let mask = (1N << chunk_bits) - 1N
+ let mut acc = 0N
+ let mut x = a
+ let mut shift_a = 0
+ while x > 0N {
+ let xa = x & mask
+ if xa > 0N {
+ let mut y = b
+ let mut shift_b = 0
+ while y > 0N {
+ let yb = y & mask
+ if yb > 0N {
+ acc += (xa * yb) << (shift_a + shift_b)
+ }
+ y = y >> chunk_bits
+ shift_b += chunk_bits
+ }
+ }
+ x = x >> chunk_bits
+ shift_a += chunk_bits
+ }
+ acc
+}
+
+///|
+test "multiplication straddling the Karatsuba threshold" {
+ // dense sweep right at the cutoff (50 limbs), where the operand split
+ // and recombination is most likely to go wrong
+ for la in 47..<54 {
+ for lb in 47..<54 {
+ let a = mag_of_limbs(seed_u64(la * 100 + lb), la)
+ let b = mag_of_limbs(seed_u64(la * 313 + lb) + 1UL, lb)
+ assert_eq(a * b, chunked_mul(a, b))
+ // all-ones magnitude: every partial product carries
+ let ones = (1N << (32 * la)) - 1N
+ assert_eq(ones * b, chunked_mul(ones, b))
+ }
+ }
+ // asymmetric and large sizes, plus the sign table
+ let sizes : Array[(Int, Int)] = [
+ (50, 99),
+ (99, 50),
+ (100, 100),
+ (150, 77),
+ (255, 256),
+ (300, 129),
+ ]
+ for si in 0.. {
+ let (s, i, j, k) = input
+ let szs : Array[Int] = [1, 2, 49, 50, 51, 120]
+ let sa = szs[(i & 0x7fffffff) % szs.length()]
+ let sb = szs[(j & 0x7fffffff) % szs.length()]
+ let sc = szs[(k & 0x7fffffff) % szs.length()]
+ let seed = seed_u64(s) * 0x9E3779B9UL
+ let a = mag_of_limbs(seed + 1UL, sa)
+ let b = mag_of_limbs(seed + 2UL, sb)
+ let c = mag_of_limbs(seed + 3UL, sc)
+ a * (b + c) == a * b + a * c &&
+ a + b - b == a &&
+ a * b / b == a &&
+ (a * b % b).is_zero()
+ },
+ count=60,
+ )
+}
+
+///|
+test "quickcheck: truncated division law across mixed size classes" {
+ @quickcheck.check(
+ (input : (Int, Int, Int, Bool, Bool)) => {
+ let (s, i, j, na, nb) = input
+ let la = 1 + (i & 63) // 1..64 limbs
+ let lb = 1 + (j & 63)
+ let seed = seed_u64(s) * 0xDEADBEEFUL + 12345UL
+ let a0 = mag_of_limbs(seed, la)
+ let b0 = mag_of_limbs(seed + 424242UL, lb)
+ let a = if na { -a0 } else { a0 }
+ let b = if nb { -b0 } else { b0 }
+ let q = a / b
+ let r = a % b
+ guard q * b + r == a else { return false }
+ let abs_r = if r < 0N { -r } else { r }
+ let abs_b = if b < 0N { -b } else { b }
+ guard abs_r < abs_b else { return false }
+ // truncated convention: remainder takes the dividend's sign,
+ // quotient rounds toward zero
+ guard r.is_zero() || (r < 0N) == (a < 0N) else { return false }
+ guard q.is_zero() || (q < 0N) == ((a < 0N) != (b < 0N)) else {
+ return false
+ }
+ true
+ },
+ count=120,
+ )
+}
+
+///|
+test "division with adversarial divisor top limbs" {
+ // Knuth 4.3.1 quotient-digit estimation is most fragile when the
+ // normalized divisor's top limb is minimal (0x80000000) or maximal
+ // (0xffffffff) and the quotient digits are all-ones. Exact roundtrip
+ // (q*b + r) / b == q with 0 <= r < b catches any mis-estimation.
+ let bs : Array[@bigint.BigInt] = []
+ for blen in ([2, 3, 5, 20, 60] : Array[_]) {
+ bs.push(1N << (32 * blen - 1)) // 0x80000000 0...0
+ bs.push((1N << (32 * blen - 1)) + 1N)
+ bs.push((1N << (32 * blen)) - 1N) // all 0xffffffff
+ bs.push((1N << (32 * blen - 1)) + ((1N << (32 * (blen - 1))) - 1N))
+ bs.push((1N << (32 * blen)) - (1N << 32)) // 0xff.. with a zero low limb
+ }
+ let qs : Array[@bigint.BigInt] = []
+ for qlen in ([1, 2, 3, 51, 70] : Array[_]) {
+ qs.push((1N << (32 * qlen)) - 1N)
+ qs.push(1N << (32 * qlen - 1))
+ qs.push((1N << (32 * qlen - 1)) + 1N)
+ }
+ for b in bs {
+ for q in qs {
+ for r in ([0N, 1N, b - 1N, b >> 1, b - 2N] : Array[_]) {
+ let a = q * b + r
+ assert_eq(a / b, q)
+ assert_eq(a % b, r)
+ }
+ }
+ }
+}
+
+///|
+test "division qh estimation corners, three-limb divisors" {
+ let pats : Array[UInt] = [
+ 0U, 1U, 0x7fffffffU, 0x80000000U, 0x80000001U, 0xffffffffU,
+ ]
+ let big_qs : Array[@bigint.BigInt] = [
+ (1N << 1920) - 1N, // 60 all-ones limbs
+ (1N << 1600) + 1N,
+ ((1N << 96) - 1N) << 512,
+ ]
+ for v1 in pats {
+ if v1 == 0 {
+ continue
+ }
+ for v2 in pats {
+ for v3 in pats {
+ let b = (@bigint.BigInt::from_uint(v1) << 64) |
+ (@bigint.BigInt::from_uint(v2) << 32) |
+ @bigint.BigInt::from_uint(v3)
+ let qs : Array[@bigint.BigInt] = []
+ for q1 in pats {
+ if q1 == 0 {
+ continue
+ }
+ qs.push(
+ (@bigint.BigInt::from_uint(q1) << 32) |
+ @bigint.BigInt::from_uint(0xffffffffU),
+ )
+ }
+ for bq in big_qs {
+ qs.push(bq)
+ }
+ for q in qs {
+ for r in ([0N, 1N, b - 1N, b >> 1] : Array[_]) {
+ let a = q * b + r
+ assert_eq(a / b, q)
+ assert_eq(a % b, r)
+ }
+ }
+ }
+ }
+ }
+}
+
+///|
+test "shifts against multiplication and floor division" {
+ let ks : Array[Int] = [1, 31, 32, 33, 63, 64, 65, 95, 1023, 1024, 1025, 1600]
+ for t in 0..<4 {
+ let m = mag_of_limbs(0xBEEF00UL + seed_u64(t), 40 + t * 17)
+ for a in ([m, -m] : Array[_]) {
+ for k in ks {
+ let p = 1N << k
+ assert_eq(a << k, a * p)
+ // a >> k is the floor: the unique q with q*2^k <= a < (q+1)*2^k
+ let q = a >> k
+ assert_true(q * p <= a)
+ assert_true(a < q * p + p)
+ }
+ }
+ }
+ // extremes and exact limb-width multiples on negative values
+ assert_eq(5N >> 100000, 0N)
+ assert_eq(-5N >> 100000, -1N)
+ assert_eq(-1N >> 1, -1N)
+ assert_eq(-2N >> 1, -1N)
+ assert_eq(-3N >> 1, -2N)
+ let v = -((1N << 96) + 1N)
+ assert_eq(v >> 32, -((1N << 64) + 1N))
+ assert_eq(v >> 96, -2N)
+ assert_eq(-(1N << 96) >> 96, -1N)
+}
+
+///|
+test "radix roundtrip for every base 2..36" {
+ for base in 2..<=36 {
+ for t in 0..<3 {
+ let limbs = [1, 8, 40][t]
+ let m = mag_of_limbs(seed_u64(base * 100 + t) + 0xD00DUL, limbs)
+ for a in ([m, -m] : Array[_]) {
+ let s = a.to_string(radix=base)
+ assert_eq(@bigint.BigInt::from_string(s, radix=base), a)
+ }
+ }
+ }
+}
+
+///|
+test "huge decimal roundtrip anchored to a hex literal" {
+ // the hex printer is plain bit manipulation, so a parse/print pair that
+ // reproduces the original 800-digit hex string anchors the value; the
+ // decimal round-trip then exercises the batched base-10^7 converter and
+ // the per-digit decimal parser against each other at ~500 limbs
+ let digits = "0123456789abcdef"
+ let sb = StringBuilder()
+ for i in 0..<800 {
+ let d = (mix64(0xFACEUL + seed_u64(i)) & 15UL).to_int()
+ let d = if i == 0 && d == 0 { 15 } else { d }
+ sb.write_char(digits.get_char(d).unwrap())
+ }
+ let hex = sb.to_string()
+ let a = @bigint.BigInt::from_string(hex, radix=16)
+ assert_eq(a.to_string(radix=16), hex)
+ assert_eq(@bigint.BigInt::from_string(a.to_string()), a)
+ let big = mag_of_limbs(0xC0FFEEUL, 500)
+ assert_eq(@bigint.BigInt::from_string(big.to_string()), big)
+ assert_eq(@bigint.BigInt::from_string((-big).to_string()), -big)
+}
+
+///|
+test "pow against iterated multiplication, modpow against reduction" {
+ for t in 0..<4 {
+ let base0 = mag_of_limbs(0xAB0BAUL + seed_u64(t) * 131UL, 3)
+ for b in ([base0, -base0] : Array[_]) {
+ let m = mag_of_limbs(0x60D5EEDUL + seed_u64(t), 2)
+ let mut acc = 1N
+ for e in 0..<9 {
+ let eb = @bigint.BigInt::from_int(e)
+ assert_eq(b.pow(eb), acc)
+ // modpow result is canonical: in [0, m) even for negative bases
+ assert_eq(b.pow(eb, modulus=m), (acc % m + m) % m)
+ acc = acc * b
+ }
+ }
+ }
+}
+
+///|
+test "octets roundtrip at every length" {
+ let lens : Array[Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 63, 64, 65]
+ for li in 0.. {
+ let b = (mix64(0x0C7E75UL + seed_u64(li * 1000 + i)) & 0xffUL).to_int()
+ let b = if i == 0 && b == 0 { 1 } else { b }
+ b.to_byte()
+ })
+ let x = @bigint.BigInt::from_octets(bytes)
+ assert_eq(x.to_octets(length=n), bytes)
+ assert_eq(x.to_octets(), bytes)
+ }
+}
+
+///|
+test "fixed-width conversion boundaries and truncation" {
+ let int64s : Array[Int64] = [
+ 0L, 1L, -1L, 2147483647L, -2147483648L, 2147483648L, -2147483649L, 4294967295L,
+ 4294967296L, -4294967296L, 9223372036854775807L, -9223372036854775808L,
+ ]
+ for x in int64s {
+ let b = @bigint.BigInt::from_int64(x)
+ assert_eq(b.to_int64(), x)
+ assert_eq(@bigint.BigInt::from_string(x.to_string()), b)
+ }
+ let uint64s : Array[UInt64] = [
+ 0UL, 1UL, 4294967295UL, 4294967296UL, 9223372036854775807UL, 9223372036854775808UL,
+ 18446744073709551615UL,
+ ]
+ for u in uint64s {
+ let b = @bigint.BigInt::from_uint64(u)
+ assert_eq(b.to_uint64(), u)
+ }
+ // truncation keeps the low bits in two's complement — regression for
+ // the magnitude-limb defect on negatives beyond one modulus
+ assert_eq(((1N << 80) + 5N).to_uint64(), 5UL)
+ assert_eq((-(1N << 80) - 5N).to_uint64(), 0UL - 5UL)
+ assert_eq((-(1N << 65) - 5N).to_uint64(), 0UL - 5UL)
+ assert_eq(((1N << 80) + 5N).to_uint(), 5U)
+ assert_eq((-(1N << 80) - 5N).to_uint(), 0U - 5U)
+ assert_eq((-(1N << 33) - 5N).to_uint(), 0U - 5U)
+ assert_eq((-(1N << 34)).to_uint(), 0U)
+ assert_eq((-(1N << 33) - 4294967295N).to_uint(), 1U)
+}
+
+///|
+test "comparison sign agrees with subtraction" {
+ let sizes : Array[Int] = [1, 2, 3, 40, 60, 100]
+ for si in 0.. @bigint.parse_bigint(s)) is Failure::Failure(_),
+ )
+ }
+ let bad16 : Array[String] = [
+ "", "-", "g", "0x5", " f", "f ", "+f", "f_f", "--f",
+ ]
+ for s in bad16 {
+ assert_true(
+ @test.expect_error(() => @bigint.parse_bigint(s, base=16))
+ is Failure::Failure(_),
+ )
+ }
+ inspect(@bigint.parse_bigint("0005"), content="5")
+ inspect(@bigint.parse_bigint("-0"), content="0")
+ inspect(@bigint.parse_bigint("-0000"), content="0")
+ inspect(@bigint.parse_bigint("00ff", base=16), content="255")
+ inspect(@bigint.parse_bigint("-00FF", base=16), content="-255")
+ inspect(@bigint.parse_bigint("-0000", base=16), content="0")
+ inspect(@bigint.parse_bigint("z", base=36), content="35")
+ inspect(@bigint.parse_bigint("-Z", base=36), content="-35")
+ inspect(@bigint.parse_bigint("0007", base=8), content="7")
+ inspect(@bigint.parse_bigint("-101", base=2), content="-5")
+}
diff --git a/bigint/to_string_radix_bench_test.mbt b/bigint/to_string_radix_bench_test.mbt
new file mode 100644
index 0000000000..7c0f127fc0
--- /dev/null
+++ b/bigint/to_string_radix_bench_test.mbt
@@ -0,0 +1,38 @@
+// 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.
+
+///|
+fn radix_bench_value() -> @bigint.BigInt {
+ // ~4000 bits
+ (@bigint.BigInt::from_string("7") << 4000) +
+ @bigint.BigInt::from_string("12345678901234567890")
+}
+
+///|
+test "bench bigint to_string radix=7 (4000 bits)" (it : @bench.T) {
+ let v = radix_bench_value()
+ it.bench(fn() { it.keep(v.to_string(radix=7)) })
+}
+
+///|
+test "bench bigint to_string radix=36 (4000 bits)" (it : @bench.T) {
+ let v = radix_bench_value()
+ it.bench(fn() { it.keep(v.to_string(radix=36)) })
+}
+
+///|
+test "bench bigint to_string radix=10 reference (4000 bits)" (it : @bench.T) {
+ let v = radix_bench_value()
+ it.bench(fn() { it.keep(v.to_string()) })
+}
diff --git a/bigint/truncation_regression_test.mbt b/bigint/truncation_regression_test.mbt
new file mode 100644
index 0000000000..a6649057ab
--- /dev/null
+++ b/bigint/truncation_regression_test.mbt
@@ -0,0 +1,53 @@
+// 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.
+
+// Regression tests for moonbitlang/core#4050 — the non-js
+// `to_uint`/`to_uint64` (and hence `to_int`/`to_int64`) added one
+// modulus and read sign-magnitude limbs, which is wrong whenever the
+// sum is still negative, i.e. for negative values with |x| > 2^32
+// (resp. 2^64) — already -(2^32 + 1) was mishandled; only exact
+// multiples of the modulus and scattered coincidental residues came out
+// right. js was already correct via `BigInt.asUintN`.
+
+///|
+test "to_uint truncates large negatives to two's complement" {
+ // the failure region starts right past one modulus:
+ // x mod 2^32 for x = -(2^32 + 1) is 2^32 - 1
+ inspect((-(1N << 32) - 1N).to_uint(), content="4294967295")
+ // x mod 2^32 for x = -(2^33 + 5) is 2^32 - 5 (js oracle: BigInt.asUintN)
+ inspect((-(1N << 33) - 5N).to_uint(), content="4294967291")
+ inspect((-(1N << 34)).to_uint(), content="0")
+ inspect((-(1N << 33) - 4294967295N).to_uint(), content="1")
+ inspect((-(1N << 80) - 5N).to_uint(), content="4294967291")
+ // reinterpretation must follow
+ inspect((-(1N << 33) - 5N).to_int(), content="-5")
+ // still-correct cases within one modulus
+ inspect((-5N).to_uint(), content="4294967291")
+ inspect((-4294967296N).to_uint(), content="0")
+}
+
+///|
+test "to_uint64 truncates large negatives to two's complement" {
+ // the failure region starts right past one modulus:
+ // y mod 2^64 for y = -(2^64 + 1) is 2^64 - 1
+ inspect((-(1N << 64) - 1N).to_uint64(), content="18446744073709551615")
+ // y mod 2^64 for y = -(2^65 + 5) is 2^64 - 5
+ inspect((-(1N << 65) - 5N).to_uint64(), content="18446744073709551611")
+ inspect((-(1N << 66)).to_uint64(), content="0")
+ inspect((-(1N << 80) - 5N).to_uint64(), content="18446744073709551611")
+ inspect((-(1N << 65) - 5N).to_int64(), content="-5")
+ // still-correct cases within one modulus
+ inspect((-5N).to_uint64(), content="18446744073709551611")
+ inspect((-(1N << 64)).to_uint64(), content="0")
+}
diff --git a/bool/bool_test.mbt b/bool/bool_test.mbt
index 469f42245b..0c0e4a20d3 100644
--- a/bool/bool_test.mbt
+++ b/bool/bool_test.mbt
@@ -33,8 +33,8 @@ test "Bool hash function with struct" {
// type MyInt Int derive(Show) this does not work
// let m : Map[TestHash, Char] = {}
let m = Map([])
- m[{ flag: true, x: 1 }] = '3'
- m[{ flag: false, x: 1 }] = '3'
+ m[{ flag: true, x: 1, }] = '3'
+ m[{ flag: false, x: 1, }] = '3'
debug_inspect(
m,
content=(
diff --git a/buffer/README.mbt.md b/buffer/README.mbt.md
index 390ef2be8f..8af74d220b 100644
--- a/buffer/README.mbt.md
+++ b/buffer/README.mbt.md
@@ -9,9 +9,9 @@ Create an empty buffer, optionally with a capacity hint to reduce reallocations:
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
@test.assert_eq(buf.is_empty(), true)
- let buf2 = Buffer(size_hint=1024)
+ let buf2 = @buffer.Buffer(size_hint=1024)
@test.assert_eq(buf2.length(), 0)
}
```
@@ -37,17 +37,17 @@ Write individual bytes, byte slices, or byte iterators:
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_byte(b'H')
buf.write_byte(b'i')
buf.write_bytes(b" world")
inspect(buf.to_bytes(), content="b\"Hi world\"")
// write a sub-range via BytesView
- let buf2 = Buffer()
+ let buf2 = @buffer.Buffer()
buf2.write_bytesview(b"Hello"[0:2])
inspect(buf2.to_bytes(), content="b\"He\"")
// write from an iterator
- let buf3 = Buffer()
+ let buf3 = @buffer.Buffer()
buf3.write_iter(b"ok".iter())
inspect(buf3.to_bytes(), content="b\"ok\"")
}
@@ -61,7 +61,7 @@ All integer writes come in `_be` (big-endian) and `_le` (little-endian) variants
///|
test {
// 32-bit signed/unsigned
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_int_be(0x01020304)
inspect(
buf.to_bytes(),
@@ -69,7 +69,7 @@ test {
#|b"\x01\x02\x03\x04"
),
)
- let buf2 = Buffer()
+ let buf2 = @buffer.Buffer()
buf2.write_int_le(0x01020304)
inspect(
buf2.to_bytes(),
@@ -78,7 +78,7 @@ test {
),
)
// unsigned 32-bit
- let buf3 = Buffer()
+ let buf3 = @buffer.Buffer()
buf3.write_uint_be(0xAABBU)
inspect(
buf3.to_bytes(),
@@ -95,7 +95,7 @@ test {
///|
test {
// 16-bit signed
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_int16_be((0x0102 : Int16))
buf.write_int16_le((0x0102 : Int16))
inspect(
@@ -105,7 +105,7 @@ test {
),
)
// 16-bit unsigned
- let buf2 = Buffer()
+ let buf2 = @buffer.Buffer()
buf2.write_uint16_be(Int::to_uint16(0x00AB))
inspect(
buf2.to_bytes(),
@@ -114,7 +114,7 @@ test {
),
)
// 64-bit signed
- let buf3 = Buffer()
+ let buf3 = @buffer.Buffer()
buf3.write_int64_be(0x0102030405060708L)
inspect(
buf3.to_bytes(),
@@ -123,7 +123,7 @@ test {
),
)
// 64-bit unsigned
- let buf4 = Buffer()
+ let buf4 = @buffer.Buffer()
buf4.write_uint64_le(0xAABBUL)
inspect(
buf4.to_bytes(),
@@ -139,10 +139,10 @@ test {
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_float_be(1.0)
@test.assert_eq(buf.length(), 4)
- let buf2 = Buffer()
+ let buf2 = @buffer.Buffer()
buf2.write_double_le(1.0)
@test.assert_eq(buf2.length(), 8)
}
@@ -156,16 +156,16 @@ Write individual characters or entire strings as UTF-8 or UTF-16 (LE/BE):
///|
test {
// UTF-8
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf8('A')
buf.write_string_utf8("BC")
inspect(buf.to_bytes(), content="b\"ABC\"")
// UTF-16 little-endian
- let buf2 = Buffer()
+ let buf2 = @buffer.Buffer()
buf2.write_char_utf16le('A')
inspect(buf2.to_bytes(), content="b\"A\\x00\"")
// UTF-16 big-endian
- let buf3 = Buffer()
+ let buf3 = @buffer.Buffer()
buf3.write_string_utf16be("AB")
inspect(buf3.to_bytes(), content="b\"\\x00A\\x00B\"")
}
@@ -178,7 +178,7 @@ Write any type that implements `Show` as UTF-8 bytes via `write_utf8()`. The cur
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_utf8(42)
inspect(buf.to_bytes(), content="b\"42\"")
}
@@ -191,10 +191,10 @@ Write integers in LEB128 variable-length encoding. Supported for `Int`, `UInt`,
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_leb128(624485)
inspect(buf.to_bytes(), content="b\"\\xe5\\x8e&\"")
- let buf2 = Buffer()
+ let buf2 = @buffer.Buffer()
buf2.write_leb128(300UL)
inspect(buf2.to_bytes(), content="b\"\\xac\\x02\"")
}
@@ -207,7 +207,7 @@ test {
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_bytes(b"hello")
let bytes = buf.to_bytes()
inspect(bytes, content="b\"hello\"")
@@ -223,7 +223,7 @@ test {
```mbt check
///|
test {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_bytes(b"data")
@test.assert_eq(buf.length(), 4)
buf.reset()
@@ -238,7 +238,7 @@ Pre-allocate capacity to minimize reallocations when the final size is known:
```mbt check
///|
test {
- let buf = Buffer(size_hint=1024)
+ let buf = @buffer.Buffer(size_hint=1024)
for i in 0..<100 {
buf.write_int_le(i)
}
diff --git a/buffer/buffer.mbt b/buffer/buffer.mbt
index 33afa765d4..21e8522fde 100644
--- a/buffer/buffer.mbt
+++ b/buffer/buffer.mbt
@@ -94,7 +94,7 @@ fn Buffer::grow(self : Buffer, required : Int) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf16le("Test")
/// inspect(buf.length(), content="8") // each char takes 2 bytes in UTF-16
/// }
@@ -117,7 +117,7 @@ pub fn Buffer::length(self : Buffer) -> Int {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// inspect(buf.is_empty(), content="true")
/// buf.write_string_utf16le("test")
/// inspect(buf.is_empty(), content="false")
@@ -142,7 +142,7 @@ pub fn Buffer::is_empty(self : Buffer) -> Bool {
///
/// ```mbt check
/// test {
-/// let buf = Buffer(size_hint=10)
+/// let buf = @buffer.Buffer(size_hint=10)
/// inspect(buf.length(), content="0")
/// buf.write_string_utf16le("test")
/// inspect(buf.length(), content="8")
@@ -168,7 +168,7 @@ pub fn new(size_hint? : Int = 0) -> Buffer {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf16le("test")
/// inspect(buf.length(), content="8")
/// }
@@ -176,7 +176,7 @@ pub fn new(size_hint? : Int = 0) -> Buffer {
pub fn Buffer::Buffer(size_hint? : Int = 0) -> Buffer {
let initial = if size_hint < 1 { 1 } else { size_hint }
let data = FixedArray::make(initial, b'\x00')
- { data, len: 0 }
+ { data, len: 0, }
}
///|
@@ -195,12 +195,16 @@ pub fn from_bytes(bytes : BytesView) -> Buffer {
/// Create a buffer from an array.
pub fn from_array(arr : ArrayView[Byte]) -> Buffer {
let buf = Buffer(size_hint=arr.length())
+ // Inline write_byte because the exact capacity is known.
+ // SAFETY: the buffer was sized for `arr.length()` bytes, so every index
+ // below stays within `data`.
+ let data = buf.data
+ let mut len = buf.len
for byte in arr {
- // Inline write_byte because the exact capacity is known.
- // SAFETY: known array size
- buf.data[buf.len] = byte
- buf.len += 1
+ data.unsafe_set(len, byte)
+ len += 1
}
+ buf.len = len
buf
}
@@ -216,7 +220,9 @@ pub fn from_iter(iter : Iter[Byte]) -> Buffer {
} else {
capacity
}
- buf.data[buf.len] = byte
+ // SAFETY: `capacity` tracks `data.length()` and the branch above ensures
+ // `len < capacity`.
+ buf.data.unsafe_set(buf.len, byte)
buf.len += 1
continue capacity
}
@@ -260,7 +266,7 @@ pub impl Logger for Buffer with fn write_string(self, value) {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_uint64_be(0xAABBCCDD11223344)
/// // Bytes are written in big-endian order
/// inspect(
@@ -275,15 +281,17 @@ pub fn Buffer::write_uint64_be(self : Buffer, value : UInt64) -> Unit {
if self.data.length() - self.len < 8 {
self.grow(self.len + 8)
}
+ // SAFETY: the guard above reserves 8 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = (value >> 56).to_byte()
- self.data[offset + 1] = (value >> 48).to_byte()
- self.data[offset + 2] = (value >> 40).to_byte()
- self.data[offset + 3] = (value >> 32).to_byte()
- self.data[offset + 4] = (value >> 24).to_byte()
- self.data[offset + 5] = (value >> 16).to_byte()
- self.data[offset + 6] = (value >> 8).to_byte()
- self.data[offset + 7] = value.to_byte()
+ data.unsafe_set(offset, (value >> 56).to_byte())
+ data.unsafe_set(offset + 1, (value >> 48).to_byte())
+ data.unsafe_set(offset + 2, (value >> 40).to_byte())
+ data.unsafe_set(offset + 3, (value >> 32).to_byte())
+ data.unsafe_set(offset + 4, (value >> 24).to_byte())
+ data.unsafe_set(offset + 5, (value >> 16).to_byte())
+ data.unsafe_set(offset + 6, (value >> 8).to_byte())
+ data.unsafe_set(offset + 7, value.to_byte())
self.len += 8
}
@@ -300,7 +308,7 @@ pub fn Buffer::write_uint64_be(self : Buffer, value : UInt64) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_uint64_le(0x0123456789ABCDEF)
/// inspect(
/// buf.contents(),
@@ -314,15 +322,17 @@ pub fn Buffer::write_uint64_le(self : Buffer, value : UInt64) -> Unit {
if self.data.length() - self.len < 8 {
self.grow(self.len + 8)
}
+ // SAFETY: the guard above reserves 8 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = value.to_byte()
- self.data[offset + 1] = (value >> 8).to_byte()
- self.data[offset + 2] = (value >> 16).to_byte()
- self.data[offset + 3] = (value >> 24).to_byte()
- self.data[offset + 4] = (value >> 32).to_byte()
- self.data[offset + 5] = (value >> 40).to_byte()
- self.data[offset + 6] = (value >> 48).to_byte()
- self.data[offset + 7] = (value >> 56).to_byte()
+ data.unsafe_set(offset, value.to_byte())
+ data.unsafe_set(offset + 1, (value >> 8).to_byte())
+ data.unsafe_set(offset + 2, (value >> 16).to_byte())
+ data.unsafe_set(offset + 3, (value >> 24).to_byte())
+ data.unsafe_set(offset + 4, (value >> 32).to_byte())
+ data.unsafe_set(offset + 5, (value >> 40).to_byte())
+ data.unsafe_set(offset + 6, (value >> 48).to_byte())
+ data.unsafe_set(offset + 7, (value >> 56).to_byte())
self.len += 8
}
@@ -339,7 +349,7 @@ pub fn Buffer::write_uint64_le(self : Buffer, value : UInt64) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_int64_be(0x0102030405060708L)
/// inspect(
/// buf.contents(),
@@ -365,7 +375,7 @@ pub fn Buffer::write_int64_be(self : Buffer, value : Int64) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_int64_le(-1L)
/// inspect(
/// buf.contents(),
@@ -392,7 +402,7 @@ pub fn Buffer::write_int64_le(self : Buffer, value : Int64) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_uint_be(0x12345678)
/// inspect(
/// buf.contents(),
@@ -406,11 +416,13 @@ pub fn Buffer::write_uint_be(self : Buffer, value : UInt) -> Unit {
if self.data.length() - self.len < 4 {
self.grow(self.len + 4)
}
+ // SAFETY: the guard above reserves 4 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = (value >> 24).to_byte()
- self.data[offset + 1] = (value >> 16).to_byte()
- self.data[offset + 2] = (value >> 8).to_byte()
- self.data[offset + 3] = value.to_byte()
+ data.unsafe_set(offset, (value >> 24).to_byte())
+ data.unsafe_set(offset + 1, (value >> 16).to_byte())
+ data.unsafe_set(offset + 2, (value >> 8).to_byte())
+ data.unsafe_set(offset + 3, value.to_byte())
self.len += 4
}
@@ -428,7 +440,7 @@ pub fn Buffer::write_uint_be(self : Buffer, value : UInt) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_uint_le(0x12345678)
/// inspect(
/// buf.contents(),
@@ -442,11 +454,13 @@ pub fn Buffer::write_uint_le(self : Buffer, value : UInt) -> Unit {
if self.data.length() - self.len < 4 {
self.grow(self.len + 4)
}
+ // SAFETY: the guard above reserves 4 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = value.to_byte()
- self.data[offset + 1] = (value >> 8).to_byte()
- self.data[offset + 2] = (value >> 16).to_byte()
- self.data[offset + 3] = (value >> 24).to_byte()
+ data.unsafe_set(offset, value.to_byte())
+ data.unsafe_set(offset + 1, (value >> 8).to_byte())
+ data.unsafe_set(offset + 2, (value >> 16).to_byte())
+ data.unsafe_set(offset + 3, (value >> 24).to_byte())
self.len += 4
}
@@ -463,7 +477,7 @@ pub fn Buffer::write_uint_le(self : Buffer, value : UInt) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_int_be(0x12345678)
/// inspect(
/// buf.contents(),
@@ -491,7 +505,7 @@ pub fn Buffer::write_int_be(self : Buffer, value : Int) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_int_le(-1)
/// inspect(buf.contents(), content="b\"\\xff\\xff\\xff\\xff\"")
/// }
@@ -513,7 +527,7 @@ pub fn Buffer::write_int_le(self : Buffer, value : Int) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_uint16_be(0x1234)
/// inspect(
/// buf.contents(),
@@ -527,9 +541,11 @@ pub fn Buffer::write_uint16_be(self : Buffer, value : UInt16) -> Unit {
if self.data.length() - self.len < 2 {
self.grow(self.len + 2)
}
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = (value.to_int() >> 8).to_byte()
- self.data[offset + 1] = value.to_byte()
+ data.unsafe_set(offset, (value.to_int() >> 8).to_byte())
+ data.unsafe_set(offset + 1, value.to_byte())
self.len += 2
}
@@ -547,7 +563,7 @@ pub fn Buffer::write_uint16_be(self : Buffer, value : UInt16) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_uint16_le(0x1234)
/// inspect(
/// buf.contents(),
@@ -561,9 +577,11 @@ pub fn Buffer::write_uint16_le(self : Buffer, value : UInt16) -> Unit {
if self.data.length() - self.len < 2 {
self.grow(self.len + 2)
}
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = value.to_byte()
- self.data[offset + 1] = (value.to_int() >> 8).to_byte()
+ data.unsafe_set(offset, value.to_byte())
+ data.unsafe_set(offset + 1, (value.to_int() >> 8).to_byte())
self.len += 2
}
@@ -580,7 +598,7 @@ pub fn Buffer::write_uint16_le(self : Buffer, value : UInt16) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_int16_be(0x1234)
/// inspect(
/// buf.contents(),
@@ -594,9 +612,11 @@ pub fn Buffer::write_int16_be(self : Buffer, value : Int16) -> Unit {
if self.data.length() - self.len < 2 {
self.grow(self.len + 2)
}
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = (value.to_int() >> 8).to_byte()
- self.data[offset + 1] = value.to_byte()
+ data.unsafe_set(offset, (value.to_int() >> 8).to_byte())
+ data.unsafe_set(offset + 1, value.to_byte())
self.len += 2
}
@@ -613,7 +633,7 @@ pub fn Buffer::write_int16_be(self : Buffer, value : Int16) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_int16_le(-1)
/// inspect(
/// buf.contents(),
@@ -627,9 +647,11 @@ pub fn Buffer::write_int16_le(self : Buffer, value : Int16) -> Unit {
if self.data.length() - self.len < 2 {
self.grow(self.len + 2)
}
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = self.data
let offset = self.len
- self.data[offset] = value.to_byte()
- self.data[offset + 1] = (value.to_int() >> 8).to_byte()
+ data.unsafe_set(offset, value.to_byte())
+ data.unsafe_set(offset + 1, (value.to_int() >> 8).to_byte())
self.len += 2
}
@@ -646,7 +668,7 @@ pub fn Buffer::write_int16_le(self : Buffer, value : Int16) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_double_be(1.0)
/// inspect(
/// buf.contents(),
@@ -673,7 +695,7 @@ pub fn Buffer::write_double_be(self : Buffer, value : Double) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_double_le(3.14)
/// inspect(
/// buf.contents(),
@@ -701,7 +723,7 @@ pub fn Buffer::write_double_le(self : Buffer, value : Double) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_float_be(3.14)
/// // In big-endian format, 3.14 is represented as [0x40, 0x48, 0xF5, 0xC3]
/// inspect(
@@ -729,7 +751,7 @@ pub fn Buffer::write_float_be(self : Buffer, value : Float) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_float_le(3.14)
/// // The bytes are written in little-endian format
/// inspect(
@@ -773,7 +795,7 @@ pub fn Buffer::write_object(self : Buffer, value : &Show) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_utf8(42)
/// inspect(
/// buf.contents(),
@@ -800,7 +822,7 @@ pub fn[T : Show] Buffer::write_utf8(self : Buffer, value : T) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_bytes(b"Test")
/// inspect(
/// buf.contents(),
@@ -826,7 +848,7 @@ pub fn Buffer::write_bytes(self : Buffer, value : BytesView) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// let view = b"Test"[1:3]
/// buf.write_bytesview(view)
/// inspect(
@@ -861,34 +883,44 @@ pub fn Buffer::write_char_utf8(buf : Self, value : Char) -> Unit {
if buf.len >= buf.data.length() {
buf.grow(buf.len + 1)
}
- buf.data[buf.len] = ((code & 0x7F) | 0x00).to_byte()
+ // SAFETY: the guard above reserves 1 byte at `len`.
+ buf.data.unsafe_set(buf.len, ((code & 0x7F) | 0x00).to_byte())
buf.len += 1
}
_..<0x0800 => {
if buf.data.length() - buf.len < 2 {
buf.grow(buf.len + 2)
}
- buf.data[buf.len] = (((code >> 6) & 0x1F) | 0xC0).to_byte()
- buf.data[buf.len + 1] = ((code & 0x3F) | 0x80).to_byte()
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (((code >> 6) & 0x1F) | 0xC0).to_byte())
+ data.unsafe_set(offset + 1, ((code & 0x3F) | 0x80).to_byte())
buf.len += 2
}
_..<0x010000 => {
if buf.data.length() - buf.len < 3 {
buf.grow(buf.len + 3)
}
- buf.data[buf.len] = (((code >> 12) & 0x0F) | 0xE0).to_byte()
- buf.data[buf.len + 1] = (((code >> 6) & 0x3F) | 0x80).to_byte()
- buf.data[buf.len + 2] = ((code & 0x3F) | 0x80).to_byte()
+ // SAFETY: the guard above reserves 3 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (((code >> 12) & 0x0F) | 0xE0).to_byte())
+ data.unsafe_set(offset + 1, (((code >> 6) & 0x3F) | 0x80).to_byte())
+ data.unsafe_set(offset + 2, ((code & 0x3F) | 0x80).to_byte())
buf.len += 3
}
_..<0x110000 => {
if buf.data.length() - buf.len < 4 {
buf.grow(buf.len + 4)
}
- buf.data[buf.len] = (((code >> 18) & 0x07) | 0xF0).to_byte()
- buf.data[buf.len + 1] = (((code >> 12) & 0x3F) | 0x80).to_byte()
- buf.data[buf.len + 2] = (((code >> 6) & 0x3F) | 0x80).to_byte()
- buf.data[buf.len + 3] = ((code & 0x3F) | 0x80).to_byte()
+ // SAFETY: the guard above reserves 4 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (((code >> 18) & 0x07) | 0xF0).to_byte())
+ data.unsafe_set(offset + 1, (((code >> 12) & 0x3F) | 0x80).to_byte())
+ data.unsafe_set(offset + 2, (((code >> 6) & 0x3F) | 0x80).to_byte())
+ data.unsafe_set(offset + 3, ((code & 0x3F) | 0x80).to_byte())
buf.len += 4
}
_ => abort("Char out of range")
@@ -903,8 +935,11 @@ pub fn Buffer::write_char_utf16le(buf : Self, value : Char) -> Unit {
if buf.data.length() - buf.len < 2 {
buf.grow(buf.len + 2)
}
- buf.data[buf.len + 0] = (code & 0xFF).to_byte()
- buf.data[buf.len + 1] = (code >> 8).to_byte()
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (code & 0xFF).to_byte())
+ data.unsafe_set(offset + 1, (code >> 8).to_byte())
buf.len += 2
} else if code < 0x110000 {
let cp = code - 0x10000
@@ -913,10 +948,13 @@ pub fn Buffer::write_char_utf16le(buf : Self, value : Char) -> Unit {
if buf.data.length() - buf.len < 4 {
buf.grow(buf.len + 4)
}
- buf.data[buf.len + 0] = (high & 0xFF).to_byte()
- buf.data[buf.len + 1] = (high >> 8).to_byte()
- buf.data[buf.len + 2] = (low & 0xFF).to_byte()
- buf.data[buf.len + 3] = (low >> 8).to_byte()
+ // SAFETY: the guard above reserves 4 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (high & 0xFF).to_byte())
+ data.unsafe_set(offset + 1, (high >> 8).to_byte())
+ data.unsafe_set(offset + 2, (low & 0xFF).to_byte())
+ data.unsafe_set(offset + 3, (low >> 8).to_byte())
buf.len += 4
} else {
abort("Char out of range")
@@ -931,8 +969,11 @@ pub fn Buffer::write_char_utf16be(buf : Self, value : Char) -> Unit {
if buf.data.length() - buf.len < 2 {
buf.grow(buf.len + 2)
}
- buf.data[buf.len + 0] = (code >> 8).to_byte()
- buf.data[buf.len + 1] = (code & 0xFF).to_byte()
+ // SAFETY: the guard above reserves 2 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (code >> 8).to_byte())
+ data.unsafe_set(offset + 1, (code & 0xFF).to_byte())
buf.len += 2
} else if code < 0x110000 {
if buf.data.length() - buf.len < 4 {
@@ -941,10 +982,13 @@ pub fn Buffer::write_char_utf16be(buf : Self, value : Char) -> Unit {
let cp = code - 0x10000
let high = (cp >> 10) | 0xD800
let low = (cp & 0x3FF) | 0xDC00
- buf.data[buf.len + 0] = (high >> 8).to_byte()
- buf.data[buf.len + 1] = (high & 0xFF).to_byte()
- buf.data[buf.len + 2] = (low >> 8).to_byte()
- buf.data[buf.len + 3] = (low & 0xFF).to_byte()
+ // SAFETY: the guard above reserves 4 bytes at `len`.
+ let data = buf.data
+ let offset = buf.len
+ data.unsafe_set(offset, (high >> 8).to_byte())
+ data.unsafe_set(offset + 1, (high & 0xFF).to_byte())
+ data.unsafe_set(offset + 2, (low >> 8).to_byte())
+ data.unsafe_set(offset + 3, (low & 0xFF).to_byte())
buf.len += 4
} else {
abort("Char out of range")
@@ -963,7 +1007,7 @@ pub fn Buffer::write_char_utf16be(buf : Self, value : Char) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf8("Hi")
/// inspect(buf.contents().length(), content="2")
/// }
@@ -986,7 +1030,7 @@ pub fn Buffer::write_string_utf8(buf : Self, string : StringView) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf16le("A")
/// inspect(buf.contents().length(), content="2")
/// }
@@ -998,10 +1042,13 @@ pub fn Buffer::write_string_utf16le(buf : Self, string : StringView) -> Unit {
if required > buf.data.length() || required < buf.len {
buf.grow(required)
}
+ // SAFETY: the guard above reserves `len * 2` bytes at `buf.len`, and
+ // `code_units()` yields exactly `len` code units.
+ let data = buf.data
for code_unit in string.code_units(); j = buf.len {
let c = code_unit.to_int().reinterpret_as_uint()
- buf.data[j] = (c & 0xff).to_byte()
- buf.data[j + 1] = (c >> 8).to_byte()
+ data.unsafe_set(j, (c & 0xff).to_byte())
+ data.unsafe_set(j + 1, (c >> 8).to_byte())
continue j + 2
}
buf.len += len * 2
@@ -1019,7 +1066,7 @@ pub fn Buffer::write_string_utf16le(buf : Self, string : StringView) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf16be("A")
/// inspect(buf.contents().length(), content="2")
/// }
@@ -1030,10 +1077,13 @@ pub fn Buffer::write_string_utf16be(buf : Self, string : StringView) -> Unit {
if required > buf.data.length() || required < buf.len {
buf.grow(required)
}
+ // SAFETY: the guard above reserves `len * 2` bytes at `buf.len`, and
+ // `code_units()` yields exactly `len` code units.
+ let data = buf.data
for code_unit in string.code_units(); j = buf.len {
let c = code_unit.to_int().reinterpret_as_uint()
- buf.data[j + 1] = (c & 0xff).to_byte()
- buf.data[j] = (c >> 8).to_byte()
+ data.unsafe_set(j + 1, (c & 0xff).to_byte())
+ data.unsafe_set(j, (c >> 8).to_byte())
continue j + 2
}
buf.len += len * 2
@@ -1043,11 +1093,7 @@ pub fn Buffer::write_string_utf16be(buf : Self, string : StringView) -> Unit {
/// Parameters:
///
/// * `self` : The buffer to write to.
-/// * `str` : The source string from which the substring will be taken.
-/// * `offset` : The starting position in the source string (inclusive). Must be
-/// non-negative.
-/// * `count` : The number of characters to write. Must be non-negative and
-/// `offset + count` must not exceed the length of the source string.
+/// * `value` : The string view to be written, encoded as UTF-16LE bytes.
pub impl Logger for Buffer with fn write_view(self : Buffer, value : StringView) -> Unit {
let required = self.len + value.length() * 2
if required > self.data.length() || required < self.len {
@@ -1091,7 +1137,7 @@ pub impl Logger for Buffer with fn write_char(self : Buffer, value : Char) -> Un
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_byte(b'\x41')
/// inspect(
/// buf.contents(),
@@ -1105,7 +1151,9 @@ pub fn Buffer::write_byte(self : Buffer, value : Byte) -> Unit {
if self.len >= self.data.length() {
self.grow(self.len + 1)
}
- self.data[self.len] = value
+ // SAFETY: the guard above restores `len < data.length()`, and the buffer
+ // invariant keeps `len >= 0`.
+ self.data.unsafe_set(self.len, value)
self.len += 1
}
@@ -1121,7 +1169,7 @@ pub fn Buffer::write_byte(self : Buffer, value : Byte) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// let bytes = b"Hello"
/// buf.write_iter(bytes.iter())
/// inspect(
@@ -1150,7 +1198,7 @@ pub fn Buffer::write_iter(self : Buffer, iter : Iter[Byte]) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf16le("Hello")
/// inspect(buf.length(), content="10")
/// buf.reset()
@@ -1177,7 +1225,7 @@ pub fn Buffer::reset(self : Buffer) -> Unit {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_string_utf16le("Test")
/// let bytes = buf.to_bytes()
/// inspect(bytes.length(), content="8") //utf16
@@ -1198,7 +1246,7 @@ pub fn Buffer::to_bytes(self : Buffer) -> Bytes {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_byte(b'A')
/// let v = buf.view()
/// inspect(v.length(), content="1")
diff --git a/buffer/buffer_test.mbt b/buffer/buffer_test.mbt
index 9f57e3b512..832c05c30a 100644
--- a/buffer/buffer_test.mbt
+++ b/buffer/buffer_test.mbt
@@ -28,10 +28,10 @@ test "create buffer from array" {
///|
test "create buffer from iterator" {
- let iter = [b'5', b'\x00', b'6', b'\x00', b'7', b'\x00'].iter()
+ let iter = [|b'5', b'\x00', b'6', b'\x00', b'7', b'\x00'|]
let buf = @buffer.from_iter(iter)
inspect(buf, content="567")
- let iter = [].iter()
+ let iter = [||]
let buf = @buffer.from_iter(iter)
inspect(buf, content="")
let iter = b"0\x00".iter()
@@ -41,7 +41,7 @@ test "create buffer from iterator" {
///|
test "length method" {
- let buf = Buffer(size_hint=100)
+ let buf = @buffer.Buffer(size_hint=100)
inspect(buf.length(), content="0")
buf.write_string_utf16le("Test")
inspect(buf.length(), content="8")
@@ -49,7 +49,7 @@ test "length method" {
///|
test "is_empty method" {
- let buf = Buffer(size_hint=100)
+ let buf = @buffer.Buffer(size_hint=100)
inspect(buf.is_empty(), content="true")
buf.write_string_utf16le("Test")
inspect(buf.is_empty(), content="false")
@@ -57,14 +57,14 @@ test "is_empty method" {
///|
test "expect method with matching content" {
- let buf = Buffer(size_hint=100)
+ let buf = @buffer.Buffer(size_hint=100)
buf.write_string_utf16le("Test")
inspect(buf, content="Test")
}
///|
test "buffer grows beyond its initial capacity" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_string_utf16le(
"This is a test string that is longer than the initial capacity",
)
@@ -73,14 +73,14 @@ test "buffer grows beyond its initial capacity" {
///|
test "write UTF-16LE string view" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_string_utf16le("Hello, World!"[7:12])
inspect(buf, content="World")
}
///|
test "write_byte method" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_byte(b'A')
buf.write_byte(b'\x00')
inspect(buf, content="A")
@@ -88,7 +88,7 @@ test "write_byte method" {
///|
test "write_utf8 method" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_utf8(42)
buf.write_utf8('!')
buf.write_utf8("hi"[:])
@@ -102,7 +102,7 @@ test "write_utf8 method" {
///|
test "to_bytes method" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_string_utf16le("Test")
let bytes = buf.to_bytes()
inspect(bytes.length(), content="8") // Each character in "Test" is 2 bytes
@@ -110,7 +110,7 @@ test "to_bytes method" {
///|
test "write_bytes" {
- let buf = Buffer(size_hint=4)
+ let buf = @buffer.Buffer(size_hint=4)
buf.write_bytes(b"1\x002\x003\x004\x00")
buf.write_bytes(b"5\x006\x007\x008\x00")
inspect(buf, content="12345678")
@@ -118,7 +118,7 @@ test "write_bytes" {
///|
test "write_uint64_le method" {
- let buf = Buffer(size_hint=16)
+ let buf = @buffer.Buffer(size_hint=16)
buf.write_uint64_le(0xdeadbeefaabbccdd)
buf.write_uint64_le(0xfacefeedaabbccdd)
inspect(
@@ -131,7 +131,7 @@ test "write_uint64_le method" {
///|
test "write_uint64_be method" {
- let buf = Buffer(size_hint=16)
+ let buf = @buffer.Buffer(size_hint=16)
buf.write_uint64_be(0xdeadbeefaabbccdd)
buf.write_uint64_be(0xfacefeedaabbccdd)
inspect(
@@ -144,7 +144,7 @@ test "write_uint64_be method" {
///|
test "write_int64_le method" {
- let buf = Buffer(size_hint=16)
+ let buf = @buffer.Buffer(size_hint=16)
buf.write_int64_le(-2)
buf.write_int64_le(0xdeadbeeffacefeed)
inspect(
@@ -157,7 +157,7 @@ test "write_int64_le method" {
///|
test "write_int64_be method" {
- let buf = Buffer(size_hint=16)
+ let buf = @buffer.Buffer(size_hint=16)
buf.write_int64_be(-2)
buf.write_int64_be(0xdeadbeef)
inspect(
@@ -170,7 +170,7 @@ test "write_int64_be method" {
///|
test "write_uint_le method" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_uint_le(0xdeadbeef)
buf.write_uint_le(0xdeadc0de)
inspect(
@@ -183,7 +183,7 @@ test "write_uint_le method" {
///|
test "write_uint_be method" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_uint_be(0xdeadbeef)
buf.write_uint_be(0xdeadc0de)
inspect(
@@ -196,7 +196,7 @@ test "write_uint_be method" {
///|
test "write_int_le method" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_int_le(-2)
buf.write_int_le(0xdeadbeef)
inspect(
@@ -209,7 +209,7 @@ test "write_int_le method" {
///|
test "write_int_be method" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_int_be(-2)
buf.write_int_be(0xdeadbeef)
inspect(
@@ -222,7 +222,7 @@ test "write_int_be method" {
///|
test "write_string_utf16be and view" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_string_utf16be("AZ")
inspect(
buf.to_bytes(),
@@ -235,25 +235,25 @@ test "write_string_utf16be and view" {
///|
test "panic Buffer::write_char_utf8 out of range" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf8((0x110000).unsafe_to_char())
}
///|
test "panic Buffer::write_char_utf16le out of range" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf16le((0x110000).unsafe_to_char())
}
///|
test "panic Buffer::write_char_utf16be out of range" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf16be((0x110000).unsafe_to_char())
}
///|
test "write_double_le method" {
- let buf = Buffer(size_hint=16)
+ let buf = @buffer.Buffer(size_hint=16)
buf.write_double_le(-2)
buf.write_double_le(3.14)
inspect(
@@ -266,7 +266,7 @@ test "write_double_le method" {
///|
test "write_double_be method" {
- let buf = Buffer(size_hint=16)
+ let buf = @buffer.Buffer(size_hint=16)
buf.write_double_be(-2)
buf.write_double_be(3.14)
inspect(
@@ -279,7 +279,7 @@ test "write_double_be method" {
///|
test "write_float_le method" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_float_le(-2)
buf.write_float_le(3.14)
inspect(
@@ -292,7 +292,7 @@ test "write_float_le method" {
///|
test "write_float_be method" {
- let buf = Buffer(size_hint=8)
+ let buf = @buffer.Buffer(size_hint=8)
buf.write_float_be(-2)
buf.write_float_be(3.14)
inspect(
@@ -306,7 +306,7 @@ test "write_float_be method" {
///|
test "write_iter" {
let bytes = b"hello"
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_iter(bytes.iter())
inspect(
buf.contents(),
@@ -326,7 +326,7 @@ test "write_iter" {
///|
test "write_bytesview" {
- let buf = Buffer(size_hint=4)
+ let buf = @buffer.Buffer(size_hint=4)
buf.write_bytesview(b"Test"[1:3])
inspect(
buf.contents(),
@@ -338,7 +338,7 @@ test "write_bytesview" {
///|
test "write_stringview" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_string_utf16le("hello"[1:3])
inspect(
buf.contents(),
@@ -350,7 +350,7 @@ test "write_stringview" {
///|
test "write_char_utf8" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_char_utf8('A')
buf.write_char_utf8('α')
buf.write_char_utf8('啊')
@@ -365,7 +365,7 @@ test "write_char_utf8" {
///|
test "write_char_utf16le" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_char_utf16le('A')
buf.write_char_utf16le('α')
buf.write_char_utf16le('啊')
@@ -380,7 +380,7 @@ test "write_char_utf16le" {
///|
test "write_char_utf16be" {
- let buf = Buffer(size_hint=10)
+ let buf = @buffer.Buffer(size_hint=10)
buf.write_char_utf16be('A')
buf.write_char_utf16be('α')
buf.write_char_utf16be('啊')
@@ -398,7 +398,7 @@ const BOM : Char = '\u{FEFF}'
///|
test "write_bom_utf8" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf8(BOM)
inspect(
buf.to_bytes(),
@@ -410,7 +410,7 @@ test "write_bom_utf8" {
///|
test "write_bom_utf16le" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf16le(BOM)
inspect(
buf.to_bytes(),
@@ -422,7 +422,7 @@ test "write_bom_utf16le" {
///|
test "write_bom_utf16be" {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_char_utf16be(BOM)
inspect(
buf.to_bytes(),
@@ -434,7 +434,7 @@ test "write_bom_utf16be" {
///|
test "write_uint16_le method" {
- let buf = Buffer(size_hint=4)
+ let buf = @buffer.Buffer(size_hint=4)
buf.write_uint16_le(Int::to_uint16(0x1234))
buf.write_uint16_le(Int::to_uint16(0xabcd))
inspect(
@@ -447,7 +447,7 @@ test "write_uint16_le method" {
///|
test "write_uint16_be method" {
- let buf = Buffer(size_hint=4)
+ let buf = @buffer.Buffer(size_hint=4)
buf.write_uint16_be(Int::to_uint16(0x1234))
buf.write_uint16_be(Int::to_uint16(0xabcd))
inspect(
@@ -460,7 +460,7 @@ test "write_uint16_be method" {
///|
test "write_int16_le method" {
- let buf = Buffer(size_hint=4)
+ let buf = @buffer.Buffer(size_hint=4)
buf.write_int16_le(Int16(-2))
buf.write_int16_le(Int16(0x1234))
inspect(
@@ -473,7 +473,7 @@ test "write_int16_le method" {
///|
test "write_int16_be method" {
- let buf = Buffer(size_hint=4)
+ let buf = @buffer.Buffer(size_hint=4)
buf.write_int16_be(Int16(-2))
buf.write_int16_be(Int16(0x1234))
inspect(
diff --git a/buffer/moon.pkg b/buffer/moon.pkg
index b81f463ee0..ad510f2c32 100644
--- a/buffer/moon.pkg
+++ b/buffer/moon.pkg
@@ -8,6 +8,7 @@ import {
}
import {
+ "moonbitlang/core/bench",
"moonbitlang/core/int",
"moonbitlang/core/test",
"moonbitlang/core/quickcheck",
diff --git a/buffer/quickcheck_test.mbt b/buffer/quickcheck_test.mbt
index eb938b22d3..b1b03fa07b 100644
--- a/buffer/quickcheck_test.mbt
+++ b/buffer/quickcheck_test.mbt
@@ -97,7 +97,7 @@ let sample_strings : Array[String] = [
test "quickcheck: op sequences agree with an Array[Byte] model" {
@quickcheck.check(
(ops : Array[(Int, Int)]) => {
- let buf = Buffer()
+ let buf = @buffer.Buffer()
let model : Array[Byte] = []
for op in ops {
let x = op.1
@@ -164,7 +164,7 @@ test "quickcheck: multi-width integer writes reconstruct the original value" {
@quickcheck.check(
(input : (Int, Int64, UInt, Double)) => {
let (i, l, u, d) = input
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_int_le(i)
buf.write_int_be(i)
buf.write_int64_le(l)
@@ -235,7 +235,7 @@ test "quickcheck: write_bytesview of any slice equals writing the sliced copy" {
// slices at every position.
let start = (a & 0x7FFFFFFF) % (n + 1)
let len = (b & 0x7FFFFFFF) % (n - start + 1)
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_bytesview(data[start:start + len])
let model : Array[Byte] = []
for i in start..<(start + len) {
@@ -252,7 +252,7 @@ test "quickcheck: utf16le string writes concatenate like their model" {
@quickcheck.check(
(input : (String, String)) => {
let (s1, s2) = input
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_string_utf16le(s1)
guard buf.length() == s1.length() * 2 else { return false }
buf.write_string_utf16le(s2)
@@ -271,7 +271,7 @@ test "quickcheck: reset makes the buffer behave as fresh" {
@quickcheck.check(
(input : (Bytes, Bytes)) => {
let (pre, post) = input
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_bytes(pre)
buf.write_double_be(1.5)
buf.reset()
@@ -296,7 +296,7 @@ test "quickcheck: size_hint never affects observable contents" {
let hints = [0, 1, 7, 4096]
let results : Array[Bytes] = []
for hint in hints {
- let buf = Buffer(size_hint=hint)
+ let buf = @buffer.Buffer(size_hint=hint)
for chunk in chunks {
buf.write_bytes(chunk)
buf.write_byte(b'\xAB')
@@ -319,7 +319,7 @@ test "quickcheck: contents() is non-destructive and writes append after it" {
@quickcheck.check(
(input : (Bytes, Bytes)) => {
let (first, second) = input
- let buf = Buffer()
+ let buf = @buffer.Buffer()
buf.write_bytes(first)
let c1 = buf.contents()
let c2 = buf.contents()
diff --git a/buffer/sleb128.mbt b/buffer/sleb128.mbt
index 97f813dd59..5920d02692 100644
--- a/buffer/sleb128.mbt
+++ b/buffer/sleb128.mbt
@@ -82,10 +82,11 @@ pub impl Leb128 for Int64 with fn output(self, buffer) {
}
///|
-/// Encode a value as signed LEB128 and append it to the buffer.
+/// Encode a value as LEB128 and append it to the buffer.
///
-/// This works for types implementing `Leb128`, currently including `Int` and
-/// `Int64`.
+/// This works for types implementing `Leb128`: `Int` and `Int64` are encoded as
+/// signed LEB128 (SLEB128), while `UInt` and `UInt64` are encoded as unsigned
+/// LEB128 (ULEB128).
///
/// Parameters:
///
@@ -96,7 +97,7 @@ pub impl Leb128 for Int64 with fn output(self, buffer) {
///
/// ```mbt check
/// test {
-/// let buf = Buffer()
+/// let buf = @buffer.Buffer()
/// buf.write_leb128(127)
/// inspect(buf.contents().length() > 0, content="true")
/// }
diff --git a/buffer/sleb128_test.mbt b/buffer/sleb128_test.mbt
index 4453bd4b0e..8829ca6d31 100644
--- a/buffer/sleb128_test.mbt
+++ b/buffer/sleb128_test.mbt
@@ -20,7 +20,7 @@ fn[A : Leb128] @buffer.Buffer::test_leb128(self : Self, x : A) -> Unit {
///|
test "write_leb128 Int" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
buffer.test_leb128(0)
inspect(
buffer.to_bytes(),
@@ -63,7 +63,7 @@ test "write_leb128 Int" {
///|
test "write_leb128 Int64" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
buffer.write_leb128(0L)
assert_eq(buffer.to_bytes(), b"\x00")
buffer.reset()
@@ -88,7 +88,7 @@ test "write_leb128 Int64" {
///|
test "write_leb128 UInt" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
// Test zero
buffer.write_leb128(0U)
assert_eq(buffer.to_bytes(), b"\x00")
@@ -137,7 +137,7 @@ test "write_leb128 UInt" {
///|
test "write_leb128_edge_cases" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
// Test boundary values for each byte length
buffer.write_leb128(0U)
@@ -171,7 +171,7 @@ test "write_leb128_edge_cases" {
///|
test "write_leb128_consecutive_writes" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
// Test writing multiple values consecutively
buffer.write_leb128(1U)
@@ -184,7 +184,7 @@ test "write_leb128_consecutive_writes" {
///|
test "write_leb128_negative_values" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
// Test negative values for SLEB128
buffer.write_leb128(-1)
@@ -206,7 +206,7 @@ test "write_leb128_negative_values" {
///|
test "write_leb128_negative_Int64" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
// Test negative Int64 values for SLEB128
buffer.write_leb128(-1L)
@@ -254,14 +254,14 @@ test "write_leb128_negative_Int64" {
///|
test {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
buffer.write_leb128(-65)
assert_eq(buffer.to_bytes(), b"\xbf\x7f")
}
///|
test "write_leb128 UInt64" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
buffer.write_leb128(0UL)
assert_eq(buffer.to_bytes(), b"\x00")
buffer.reset()
@@ -287,7 +287,7 @@ test "write_leb128 UInt64" {
///|
test "write_leb128 UInt64 large values" {
- let buffer = Buffer()
+ let buffer = @buffer.Buffer()
buffer.write_leb128(0xFFFFFFFFUL)
assert_eq(buffer.to_bytes(), b"\xff\xff\xff\xff\x0f")
buffer.reset()
diff --git a/buffer/write_bench_test.mbt b/buffer/write_bench_test.mbt
new file mode 100644
index 0000000000..1edd2bef85
--- /dev/null
+++ b/buffer/write_bench_test.mbt
@@ -0,0 +1,136 @@
+// 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.
+
+///|
+/// Append-path benchmarks. The buffer is allocated once outside the timed
+/// closure and rewound with `reset()`, so each run measures only the append
+/// loop: `Buffer(size_hint=n)` would otherwise charge every run a malloc plus
+/// a zero-fill of the whole capacity, on freshly handed-back memory.
+///
+/// Each run keeps the buffer itself rather than `buf.length()`: the length is
+/// derivable from the fixed loop bounds, so keeping only that would let the
+/// backend prove the byte stores unobservable and drop the loop under test.
+let buffer_bench_repeats = 4096
+
+///|
+let buffer_bench_piece = "abcdefghijklmnop"
+
+///|
+test "bench Buffer::write_byte n=4096" (it : @bench.T) {
+ let buf = @buffer.Buffer(size_hint=buffer_bench_repeats)
+ it.bench(fn() {
+ buf.reset()
+ for i in 0.. i.to_byte())
+ it.bench(fn() {
+ let buf = @buffer.from_array(arr)
+ it.keep(buf)
+ })
+}
diff --git a/builtin/LinkedHashMap.mbt.md b/builtin/LinkedHashMap.mbt.md
index 1cecfc7baf..d05167da14 100644
--- a/builtin/LinkedHashMap.mbt.md
+++ b/builtin/LinkedHashMap.mbt.md
@@ -91,7 +91,7 @@ test {
### Size & Capacity
-You can use `size()` to get the number of key-value pairs in the map, or `capacity()` to get the current capacity.
+You can use `length()` to get the number of key-value pairs in the map, or `capacity()` to get the current capacity.
```mbt check
///|
diff --git a/builtin/README.mbt.md b/builtin/README.mbt.md
index 9f45ff2557..b2082d887a 100644
--- a/builtin/README.mbt.md
+++ b/builtin/README.mbt.md
@@ -141,7 +141,7 @@ Built-in array types for storing collections:
///|
test "arrays" {
// Dynamic arrays
- let arr1 = Array::new()
+ let arr1 = Array()
arr1.push(1)
arr1.push(2)
arr1.push(3)
diff --git a/builtin/array.mbt b/builtin/array.mbt
index 36d6884435..27c26a202b 100644
--- a/builtin/array.mbt
+++ b/builtin/array.mbt
@@ -74,7 +74,7 @@ pub fn[T] Array::from_fixed_array(arr : FixedArray[T]) -> Array[T] {
/// One should use makei() instead which creates an object for each index.
#owned(elem)
pub fn[T] Array::make(len : Int, elem : T) -> Array[T] {
- let arr = Array::make_uninit(len)
+ let arr = Array::unsafe_make_uninit(len)
for i in 0.. T raise?) -> Array[T] raise? {
if length <= 0 {
[]
} else {
- let array = Array::make_uninit(length)
+ let array = Array::unsafe_make_uninit(length)
for i in 0.. T {
///
/// ```mbt check
/// test {
-/// let arr : ReadOnlyArray[Int] = [1, 2, 3]
+/// let arr : Array[Int] = [1, 2, 3]
/// inspect(arr[1], content="2")
/// }
/// ```
@@ -209,6 +209,7 @@ pub fn[T] Array::at(self : Array[T], index : Int) -> T {
/// debug_inspect(arr.get(3), content="None")
/// }
/// ```
+#intrinsic("%array.get_opt")
pub fn[T] Array::get(self : Array[T], index : Int) -> T? {
let len = self.length()
guard index >= 0 && index < len else { None }
@@ -559,6 +560,13 @@ pub fn[T] Array::any(self : Array[T], f : (T) -> Bool raise?) -> Bool raise? {
///
/// This method has no effect on the allocated capacity of the array, only setting the length to 0.
///
+/// Emptying an array is a removal like any other: the buffer keeps referring to
+/// the elements that were in it, and they are released once later pushes reuse
+/// those slots, the buffer grows, or the array is dropped. Call
+/// `Array::release_unused` to overwrite them at once, or `Array::shrink_to_fit`
+/// to hand the buffer back entirely. On the JavaScript backend the removed
+/// elements are released right away and `Array::release_unused` is a no-op.
+///
/// # Example
/// ```mbt check
/// test {
@@ -587,7 +595,7 @@ pub fn[T, U] Array::map(
self : Array[T],
f : (T) -> U raise?,
) -> Array[U] raise? {
- let arr = Array::make_uninit(self.length())
+ let arr = Array::unsafe_make_uninit(self.length())
for i, v in self {
arr.unsafe_set(i, f(v))
}
@@ -635,7 +643,7 @@ pub fn[T, U] Array::mapi(
if self.is_empty() {
return []
}
- let arr = Array::make_uninit(self.length())
+ let arr = Array::unsafe_make_uninit(self.length())
for i, v in self {
arr.unsafe_set(i, f(i, v))
}
@@ -774,7 +782,7 @@ pub fn[T] Array::rev_in_place(self : Array[T]) -> Unit {
/// ```
pub fn[T] Array::rev(self : Array[T]) -> Array[T] {
let len = self.length()
- let arr = Array::make_uninit(len)
+ let arr = Array::unsafe_make_uninit(len)
for i in 0.. (Array[T], Array[T])
len=len2,
)
} else {
- Array::make_uninit(0)
+ Array::unsafe_make_uninit(0)
}
(v1, v2)
}
@@ -1270,7 +1278,7 @@ pub fn[T] Array::flatten(self : Array[Array[T]]) -> Array[T] {
} nobreak {
len
}
- let res = Array::make_uninit(len)
+ let res = Array::unsafe_make_uninit(len)
for xs in self; i = 0 {
res.unsafe_blit(i, xs, 0, xs.length())
continue i + xs.length()
@@ -1301,7 +1309,7 @@ pub fn[T] Array::repeat(self : Array[T], times : Int) -> Array[T] {
}
let total = len * times
guard total / times == len else { abort("repeat result too large") }
- let v = Array::new(capacity=total)
+ let v = Array::Array(capacity=total)
for _ in 0.. Array[ArrayView[T]] {
/// * `predicate` : A function that takes two adjacent elements and returns
/// `true` if they should be in the same chunk, `false` otherwise.
///
-/// Returns an array of arrays, where each inner array is a chunk of consecutive
+/// Returns an array of views, where each view is a chunk of consecutive
/// elements that satisfy the predicate with their adjacent elements.
///
/// Example:
@@ -1861,6 +1869,14 @@ pub fn[A] Array::unsafe_pop_back(self : Array[A]) -> Unit {
/// - If `len` is negative, the function does nothing.
/// - If `len` exceeds current length, the array remains unchanged.
///
+/// Elements beyond `len` are removed from the array, but the backing buffer
+/// keeps referring to them: they are released once those slots are reused by
+/// later pushes, once the buffer grows, or once the array is dropped. Call
+/// `Array::release_unused` to overwrite them at once, or `Array::shrink_to_fit`
+/// to move the survivors into an exact-size buffer. On the JavaScript backend
+/// the removed elements are released right away and `Array::release_unused` is a
+/// no-op.
+///
/// Example:
///
/// ```mbt check
@@ -1924,7 +1940,7 @@ pub fn[A] Array::retain_map(
///
/// ```mbt check
/// test {
-/// let iter = Iter::singleton(42)
+/// let iter = [|42|]
/// let arr = Array::from_iter(iter)
/// debug_inspect(arr, content="[42]")
/// }
@@ -2018,15 +2034,20 @@ pub fn[T] Array::shuffle(self : Array[T], rand~ : (Int) -> Int) -> Array[T] {
}
///|
-/// Returns a new array containing the elements of the original array that satisfy the given predicate.
+/// Applies a function to each element of the array and collects the results
+/// that are `Some`, discarding the elements for which the function returns
+/// `None`.
///
/// # Arguments
///
-/// * `self` - The array to filter.
-/// * `f` - The predicate function.
+/// * `self` - The array to filter and map.
+/// * `f` - The function applied to each element, returning `Some(value)` to
+/// keep the mapped `value`, or `None` to drop the element.
///
/// # Returns
///
+/// A new array containing the unwrapped `Some` results, in the order the
+/// corresponding elements appeared in the original array.
#locals(f)
pub fn[A, B] Array::filter_map(
self : Array[A],
@@ -2111,8 +2132,8 @@ pub fn[A, B] Array::zip(self : Array[A], other : Array[B]) -> Array[(A, B)] {
/// }
/// ```
pub fn[T1, T2] Array::unzip(self : Array[(T1, T2)]) -> (Array[T1], Array[T2]) {
- let arr1 : Array[T1] = Array::new(capacity=self.length())
- let arr2 : Array[T2] = Array::new(capacity=self.length())
+ let arr1 : Array[T1] = Array(capacity=self.length())
+ let arr2 : Array[T2] = Array(capacity=self.length())
for pair in self {
let (x, y) = pair
arr1.push(x)
diff --git a/builtin/array_make_blit_bench_test.mbt b/builtin/array_make_blit_bench_test.mbt
index 6c099a5c45..c7f4513df6 100644
--- a/builtin/array_make_blit_bench_test.mbt
+++ b/builtin/array_make_blit_bench_test.mbt
@@ -116,3 +116,23 @@ test "bench Array::push Ref n=1000000 resize" (it : @bench.T) {
it.keep(data)
})
}
+
+///|
+fn make_blit_int_array(len : Int) -> Array[Int] {
+ Array::makei(len, i => i)
+}
+
+///|
+/// `Array::copy` allocates and fills in one shot. The Int and Ref variants are
+/// both kept because the element type decides whether the per-element store
+/// carries reference-counting traffic.
+test "bench Array::copy Int n=1000000" (it : @bench.T) {
+ let data = make_blit_int_array(make_blit_ref_bench_size)
+ it.bench(fn() { it.keep(data.copy()) })
+}
+
+///|
+test "bench Array::copy Ref n=1000000" (it : @bench.T) {
+ let data = make_blit_ref_array(make_blit_ref_bench_size)
+ it.bench(fn() { it.keep(data.copy()) })
+}
diff --git a/builtin/array_nonjs_test.mbt b/builtin/array_nonjs_test.mbt
index 9d7974f5a0..04199df3a1 100644
--- a/builtin/array_nonjs_test.mbt
+++ b/builtin/array_nonjs_test.mbt
@@ -14,13 +14,13 @@
///|
test "array_capacity" {
- let arr : Array[Int] = Array::new(capacity=10)
+ let arr : Array[Int] = Array(capacity=10)
assert_true(arr.capacity() >= 10)
}
///|
test "array_append_reuses_capacity" {
- let arr = Array::new(capacity=4)
+ let arr = Array(capacity=4)
arr.push(1)
arr.append([2, 3])
inspect(arr.capacity(), content="4")
@@ -29,7 +29,7 @@ test "array_append_reuses_capacity" {
///|
test "array_append_grows_geometrically" {
- let arr = Array::new(capacity=4)
+ let arr = Array(capacity=4)
arr.append([1, 2, 3, 4, 5])
inspect(arr.capacity(), content="8")
debug_inspect(arr, content="[1, 2, 3, 4, 5]")
@@ -37,7 +37,7 @@ test "array_append_grows_geometrically" {
///|
test "array_blit_to_reuses_and_grows_capacity" {
- let dst = Array::new(capacity=4)
+ let dst = Array(capacity=4)
dst.push(0)
[1, 2][:].blit_to(dst, dst_offset=1)
inspect(dst.capacity(), content="4")
@@ -59,7 +59,7 @@ test "array_retain" {
///|
test "shrink_to_fit" {
- let v = Array::new(capacity=10)
+ let v = Array(capacity=10)
v.push(1)
v.push(2)
v.push(3)
@@ -69,3 +69,209 @@ test "shrink_to_fit" {
v.shrink_to_fit()
inspect(v.capacity(), content="3")
}
+
+///|
+/// Removing elements must never leave a slot a view could read as
+/// uninitialized memory, so nothing is written to the slots a removal vacates
+/// and a view taken beforehand still sees the original elements.
+test "clear leaves the emptied slots as they were" {
+ let arr = ["a", "b", "c"]
+ let view = arr[0:3]
+ arr.clear()
+ inspect(arr.length(), content="0")
+ debug_inspect(
+ view,
+ content=(
+ #|
+ ),
+ )
+}
+
+///|
+test "release_unused after clear overwrites the whole buffer" {
+ let arr = ["a", "b", "c"]
+ let view = arr[0:3]
+ arr.clear()
+ arr.release_unused(placeholder="-")
+ inspect(arr.length(), content="0")
+ debug_inspect(
+ view,
+ content=(
+ #|
+ ),
+ )
+}
+
+///|
+test "truncate keeps removed elements reachable until release_unused" {
+ let kept = ["a", "b", "c", "d"]
+ let kept_view = kept[0:4]
+ kept.truncate(2)
+ debug_inspect(
+ kept,
+ content=(
+ #|["a", "b"]
+ ),
+ )
+ debug_inspect(
+ kept_view,
+ content=(
+ #|
+ ),
+ )
+ let filled = ["a", "b", "c", "d"]
+ let filled_view = filled[0:4]
+ filled.truncate(2)
+ filled.release_unused(placeholder="-")
+ debug_inspect(
+ filled,
+ content=(
+ #|["a", "b"]
+ ),
+ )
+ debug_inspect(
+ filled_view,
+ content=(
+ #|
+ ),
+ )
+}
+
+///|
+/// No shrinking operation replaces the buffer, so emptying an array never
+/// costs an allocation -- whether it happens through `clear`, `truncate`,
+/// `remove` or `drain`. A later push makes that observable: it lands in the
+/// reused buffer, which a view taken beforehand still points at.
+test "no shrinking operation replaces the buffer" {
+ let cleared = ["a", "b", "c"]
+ let cleared_view = cleared[0:3]
+ cleared.clear()
+ cleared.push("x")
+ debug_inspect(
+ cleared_view,
+ content=(
+ #|
+ ),
+ )
+ let truncated = ["a", "b", "c"]
+ let truncated_view = truncated[0:3]
+ truncated.truncate(0)
+ truncated.push("x")
+ debug_inspect(
+ truncated_view,
+ content=(
+ #|
+ ),
+ )
+ // `remove(0)` shifts "b" down, so the buffer reads [b, b] before the push.
+ let removed = ["a", "b"]
+ let removed_view = removed[0:2]
+ let _ = removed.remove(0)
+ removed.push("x")
+ debug_inspect(
+ removed_view,
+ content=(
+ #|
+ ),
+ )
+ // `drain(0, 2)` shifts "c" down, so the buffer reads [c, b, c] before the push.
+ let drained = ["a", "b", "c"]
+ let drained_view = drained[0:3]
+ let _ = drained.drain(0, 2)
+ drained.push("x")
+ debug_inspect(
+ drained_view,
+ content=(
+ #|
+ ),
+ )
+}
+
+///|
+test "release_unused overwrites the tail a drain vacated" {
+ let arr = ["a", "b", "c", "d"]
+ let view = arr[0:4]
+ let drained = arr.drain(1, 3)
+ arr.release_unused(placeholder="-")
+ debug_inspect(
+ drained,
+ content=(
+ #|["b", "c"]
+ ),
+ )
+ debug_inspect(
+ arr,
+ content=(
+ #|["a", "d"]
+ ),
+ )
+ debug_inspect(
+ view,
+ content=(
+ #|
+ ),
+ )
+}
+
+///|
+/// `pop`, `remove` and `retain` offer no fill value of their own, so the
+/// elements they remove stay in the buffer until something overwrites them.
+/// `release_unused` is what releases them without reallocating, and a view taken
+/// beforehand shows exactly which slots it reached.
+test "release_unused releases what pop and remove leave behind" {
+ let popped = ["a", "b", "c"]
+ let popped_view = popped[0:3]
+ let _ = popped.pop()
+ debug_inspect(
+ popped_view,
+ content=(
+ #|
+ ),
+ )
+ popped.release_unused(placeholder="-")
+ debug_inspect(
+ popped_view,
+ content=(
+ #|
+ ),
+ )
+ let removed = ["a", "b", "c"]
+ let removed_view = removed[0:3]
+ let _ = removed.remove(0)
+ removed.release_unused(placeholder="-")
+ debug_inspect(
+ removed,
+ content=(
+ #|["b", "c"]
+ ),
+ )
+ debug_inspect(
+ removed_view,
+ content=(
+ #|
+ ),
+ )
+}
+
+///|
+/// `release_unused` covers the whole unused region, not just the slots the most
+/// recent operation vacated, so one call settles a run of removals.
+test "release_unused spans every outstanding removal" {
+ let arr = ["a", "b", "c", "d"]
+ let view = arr[0:4]
+ let _ = arr.pop()
+ arr.truncate(1)
+ arr.release_unused(placeholder="-")
+ debug_inspect(
+ arr,
+ content=(
+ #|["a"]
+ ),
+ )
+ debug_inspect(
+ view,
+ content=(
+ #|
+ ),
+ )
+}
diff --git a/builtin/array_sort.mbt b/builtin/array_sort.mbt
index 8098938fa6..edc6edd8ce 100644
--- a/builtin/array_sort.mbt
+++ b/builtin/array_sort.mbt
@@ -285,7 +285,7 @@ pub fn[T : Compare] FixedArray::stable_sort(self : FixedArray[T]) -> Unit {
///
/// ```mbt check
/// test {
-/// let arr = [5, 3, 2, 4, 1]
+/// let arr : FixedArray[Int] = [5, 3, 2, 4, 1]
/// arr.sort_by_key(x => -x)
/// @test.assert_eq(arr, [5, 4, 3, 2, 1])
/// }
@@ -316,7 +316,7 @@ test "FixedArray::sort_by_key/basic" {
///
/// ```mbt check
/// test {
-/// let arr = [5, 3, 2, 4, 1]
+/// let arr : FixedArray[Int] = [5, 3, 2, 4, 1]
/// arr.sort_by((a, b) => a - b)
/// @test.assert_eq(arr, [1, 2, 3, 4, 5])
/// }
diff --git a/builtin/array_sort_impl.mbt b/builtin/array_sort_impl.mbt
index b8494044a9..38c7c3b184 100644
--- a/builtin/array_sort_impl.mbt
+++ b/builtin/array_sort_impl.mbt
@@ -48,13 +48,13 @@ fn[T : Compare] timsort(arr : MutArrayView[T]) -> Unit {
// Insert some more elements into the run if it's too short. Insertion sort is faster than
// merge sort on short sequences, so this significantly improves performance.
let end = provide_sorted_batch(arr, start, end)
- runs.push({ start, len: end - start })
+ runs.push({ start, len: end - start, })
while true {
guard collapse(runs, len) is Some(r) else { break }
let left = runs[r]
let right = runs[r + 1]
merge(arr.slice(left.start, right.start + right.len), left.len)
- runs[r + 1] = { start: left.start, len: left.len + right.len }
+ runs[r + 1] = { start: left.start, len: left.len + right.len, }
runs.remove(r) |> ignore
}
continue end, end
@@ -199,7 +199,7 @@ fn collapse(runs : Array[TimSortRun], stop : Int) -> Int? {
///
/// ```mbt check
/// test {
-/// let arr = [5, 4, 3, 2, 1]
+/// let arr : FixedArray[Int] = [5, 4, 3, 2, 1]
/// arr.sort()
/// @test.assert_eq(arr, [1, 2, 3, 4, 5])
/// }
@@ -339,11 +339,10 @@ fn[T : Compare] fixed_try_bubble_sort(arr : MutArrayView[T]) -> Bool {
}
///|
-/// Try to sort the array with bubble sort.
-///
-/// It will only tolerate at most 8 unsorted elements. The time complexity is O(n).
-///
-/// Returns whether the array is sorted.
+/// Sort the array with insertion sort (despite the name): it grows a sorted
+/// prefix, moving each new element left by adjacent swaps until it is in place.
+///
+/// It always sorts the whole array. The time complexity is O(n^2) in the worst case.
fn[T : Compare] fixed_bubble_sort(arr : MutArrayView[T]) -> Unit {
for i in 1.. 0 && arr.unsafe_get(j - 1) > arr.unsafe_get(j); j = j - 1 {
diff --git a/builtin/array_test.mbt b/builtin/array_test.mbt
index 0a687571ba..d71779fc85 100644
--- a/builtin/array_test.mbt
+++ b/builtin/array_test.mbt
@@ -14,7 +14,7 @@
///|
test "array_new" {
- let arr : Array[Int] = Array::new()
+ let arr : Array[Int] = Array()
@test.assert_eq(arr.length(), 0)
}
@@ -39,7 +39,7 @@ test "array_make" {
///|
test "array_realloc" {
- let arr = Array::new(capacity=2)
+ let arr = Array(capacity=2)
arr.push(1)
arr.push(2)
arr.push(3) // This should trigger a reallocation
@@ -149,7 +149,7 @@ test "array_append" {
// @test.assert_eq(arr1.capacity(), 6)
@test.assert_eq(arr1[3], 4)
let cap = 20
- let arr3 = Array::new(capacity=cap)
+ let arr3 = Array(capacity=cap)
arr3.resize(cap, 10)
arr1.append(arr3)
@test.assert_eq(arr1.length(), 6 + cap)
@@ -245,12 +245,7 @@ test "array_eachi" {
test "array_rev_eachi" {
let arr = ['a', 'b', 'c']
let buf = StringBuilder()
- arr.rev_eachi((i, x) => {
- buf.write_object(i)
- buf.write_string(": ")
- buf.write_object(x)
- buf.write_string("\n")
- })
+ arr.rev_eachi((i, x) => buf <+ "\{i}: \{x}\n")
inspect(
buf,
content=(
@@ -308,7 +303,7 @@ test "array_filter" {
///|
test "array_is_empty" {
- let arr : Array[Int] = Array::new()
+ let arr : Array[Int] = Array()
assert_true(arr.is_empty())
}
@@ -667,7 +662,7 @@ test "array_reserve_capacity" {
///|
test "array_shrink_to_fit" {
- let arr = Array::new(capacity=10)
+ let arr = Array(capacity=10)
arr.push(1)
arr.push(2)
arr.push(3)
@@ -746,21 +741,21 @@ struct TestStruct {
///|
test "array_binary_search_by_test" {
- let arr = [{ num: 10 }, { num: 22 }, { num: 35 }, { num: 48 }]
- let mut target = { num: 22 }
+ let arr = [{ num: 10, }, { num: 22, }, { num: 35, }, { num: 48, }]
+ let mut target = { num: 22, }
let cmp = (val : TestStruct) => val.num - target.num
@test.assert_eq(arr.binary_search_by(cmp), Ok(1))
- target = { num: 48 }
+ target = { num: 48, }
@test.assert_eq(arr.binary_search_by(cmp), Ok(3))
- target = { num: -8 }
+ target = { num: -8, }
@test.assert_eq(arr.binary_search_by(cmp), Err(0))
- target = { num: 49 }
+ target = { num: 49, }
@test.assert_eq(arr.binary_search_by(cmp), Err(4))
}
///|
test "array of bytes, new & push" {
- let bytes : Array[Byte] = Array::new(capacity=10)
+ let bytes : Array[Byte] = Array(capacity=10)
bytes.push(b'a')
debug_inspect(
bytes,
@@ -1423,7 +1418,13 @@ test "arbitrary" {
debug_inspect(
arr[5:9],
content=(
- #|
+ #|
),
)
debug_inspect(
@@ -1431,11 +1432,33 @@ test "arbitrary" {
content=(
#|
),
)
@@ -1539,7 +1562,7 @@ test "Array::unsafe_pop" {
test "Array::append/self_alias" {
// Test appending array to itself - this should double the array
// Using small capacity to force reallocation during append
- let arr = Array::new(capacity=3)
+ let arr = Array(capacity=3)
arr.push(1)
arr.push(2)
arr.push(3)
@@ -1550,7 +1573,7 @@ test "Array::append/self_alias" {
///|
test "Array::append/self_alias_partial" {
// Test appending a partial view of array to itself
- let arr = Array::new(capacity=4)
+ let arr = Array(capacity=4)
arr.push(1)
arr.push(2)
arr.push(3)
@@ -1588,7 +1611,7 @@ test "timsort_early_return_for_small_arrays" {
counter: count,
})
for i in 0..<10 {
- arr[i] = { value: 10 - i, counter: count }
+ arr[i] = { value: 10 - i, counter: count, }
}
arr.mut_view().stable_sort()
// With insertion sort only: O(n^2/2) comparisons, roughly 45 for n=10
diff --git a/builtin/array_wbtest.mbt b/builtin/array_wbtest.mbt
index 0132147da8..d28d7ce6dc 100644
--- a/builtin/array_wbtest.mbt
+++ b/builtin/array_wbtest.mbt
@@ -14,6 +14,6 @@
///|
test {
- let empty : Array[Unit] = Array::make_uninit(0)
+ let empty : Array[Unit] = Array::unsafe_make_uninit(0)
assert_true(empty == [])
}
diff --git a/builtin/arraycore_js.mbt b/builtin/arraycore_js.mbt
index 99850ce3da..5164a78762 100644
--- a/builtin/arraycore_js.mbt
+++ b/builtin/arraycore_js.mbt
@@ -106,7 +106,14 @@ extern "js" fn JSArray::copy(self : JSArray) -> JSArray =
type Array[T]
///|
-fn[T] Array::make_uninit(len : Int) -> Array[T] = "%fixedarray.make_uninit"
+/// Creates an array of `len` elements whose contents are unspecified. The
+/// array reports `length() == len` immediately, so every slot is observable
+/// and the caller must write all of them before the array is read or escapes.
+///
+/// Prefer `Array::makei` unless the initializing loop cannot be expressed as
+/// a function of the index.
+#doc(hidden)
+pub fn[T] Array::unsafe_make_uninit(len : Int) -> Array[T] = "%fixedarray.make_uninit"
///|
fn[T] Array::unsafe_make_and_blit(
@@ -116,7 +123,7 @@ fn[T] Array::unsafe_make_and_blit(
src_offset? : Int = 0,
dst_offset? : Int = 0,
) -> Array[T] {
- let dst = Array::make_uninit(allocate_len)
+ let dst = Array::unsafe_make_uninit(allocate_len)
UninitializedArray::unsafe_blit(
dst.buffer(),
dst_offset,
@@ -135,7 +142,7 @@ fn[T] Array::unsafe_make_and_blit_from_fixed(
src_offset? : Int = 0,
dst_offset? : Int = 0,
) -> Array[T] {
- let dst = Array::make_uninit(allocate_len)
+ let dst = Array::unsafe_make_uninit(allocate_len)
UninitializedArray::unsafe_blit_fixed(
dst.buffer(),
dst_offset,
@@ -162,7 +169,7 @@ fn[T] Array::unsafe_resize_with_default(
///|
/// Creates a new array.
-pub fn[T] Array::new(capacity? : Int = 0) -> Array[T] {
+pub fn[T] Array::Array(capacity? : Int = 0) -> Array[T] {
ignore(capacity)
[]
}
@@ -232,7 +239,7 @@ pub fn[T] Array::reserve_capacity(self : Array[T], capacity : Int) -> Unit {
///
/// ```mbt check
/// test {
-/// let v = Array::new(capacity=10)
+/// let v = Array(capacity=10)
/// v.push(1)
/// v.push(2)
/// v.push(3)
@@ -245,6 +252,29 @@ pub fn[T] Array::shrink_to_fit(self : Array[T]) -> Unit {
ignore(self)
}
+///|
+/// Overwrites the array's unused capacity with `placeholder`, releasing
+/// whatever those slots held.
+///
+/// **NOTE**: This method does nothing on the js platform -- shrinking a
+/// JavaScript array releases the removed elements outright, so there is no
+/// unused capacity holding on to them.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// let arr = ["a", "b", "c"]
+/// let _ = arr.pop()
+/// arr.release_unused(placeholder="")
+/// debug_inspect(arr, content="[\"a\", \"b\"]")
+/// }
+/// ```
+pub fn[T] Array::release_unused(self : Array[T], placeholder~ : T) -> Unit {
+ ignore(self)
+ ignore(placeholder)
+}
+
///|
/// Adds an element to the end of the array.
///
@@ -423,6 +453,9 @@ pub fn[T] Array::remove(self : Array[T], index : Int) -> T {
/// @test.assert_eq(v, [3, 5])
/// }
/// ```
+///
+/// On this backend the underlying JavaScript array is spliced directly, which
+/// already releases the drained elements.
pub fn[T] Array::drain(self : Array[T], begin : Int, end : Int) -> Array[T] {
guard begin >= 0 && end <= self.length() && begin <= end else {
abort(
diff --git a/builtin/arraycore_nonjs.mbt b/builtin/arraycore_nonjs.mbt
index 6977135c76..aa844ffb7d 100644
--- a/builtin/arraycore_nonjs.mbt
+++ b/builtin/arraycore_nonjs.mbt
@@ -12,9 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-///|
-fn[T] UninitializedArray::set_null(self : UninitializedArray[T], index : Int) = "%fixedarray.set_null"
-
///|
/// An `Array` is a collection of values that supports random access and can
/// grow in size.
@@ -24,8 +21,15 @@ struct Array[T] {
}
///|
-fn[T] Array::make_uninit(len : Int) -> Array[T] {
- { buf: UninitializedArray::make(len), len }
+/// Creates an array of `len` elements whose contents are unspecified. The
+/// array reports `length() == len` immediately, so every slot is observable
+/// and the caller must write all of them before the array is read or escapes.
+///
+/// Prefer `Array::makei` unless the initializing loop cannot be expressed as
+/// a function of the index.
+#doc(hidden)
+pub fn[T] Array::unsafe_make_uninit(len : Int) -> Array[T] {
+ { buf: UninitializedArray::make(len), len, }
}
///|
@@ -106,18 +110,18 @@ fn[T] Array::unsafe_resize_with_default(
///
/// ```mbt check
/// test {
-/// let arr : Array[Int] = Array::new(capacity=10)
+/// let arr : Array[Int] = Array(capacity=10)
/// inspect(arr.length(), content="0")
/// inspect(arr.capacity(), content="10")
-/// let arr : Array[Int] = Array::new()
+/// let arr : Array[Int] = Array()
/// inspect(arr.length(), content="0")
/// }
/// ```
-pub fn[T] Array::new(capacity? : Int = 0) -> Array[T] {
+pub fn[T] Array::Array(capacity? : Int = 0) -> Array[T] {
if capacity == 0 {
[]
} else {
- { buf: UninitializedArray::make(capacity), len: 0 }
+ { buf: UninitializedArray::make(capacity), len: 0, }
}
}
@@ -134,9 +138,9 @@ pub fn[T] Array::new(capacity? : Int = 0) -> Array[T] {
///
/// ```mbt check
/// test {
-/// let arr : ReadOnlyArray[Int] = [1, 2, 3]
+/// let arr : Array[Int] = [1, 2, 3]
/// inspect(arr.length(), content="3")
-/// let empty : ReadOnlyArray[Int] = []
+/// let empty : Array[Int] = []
/// inspect(empty.length(), content="0")
/// }
/// ```
@@ -162,17 +166,18 @@ pub fn[T] Array::length(self : Array[T]) -> Int {
///
/// # Errors
///
-/// - This function does not explicitly raise errors, but improper use (e.g.,
-/// setting `new_len` greater than the current length) can lead to undefined
-/// behavior.
+/// - This function does not raise errors, but it panics if `new_len` is greater
+/// than the current length of the array.
///
-/// TODO: this can be optimized by using the intrinsic to null out the range
+/// # Retention
+///
+/// The slots beyond `new_len` are left as they are rather than nulled out, so
+/// that an `ArrayView` created before the call can never observe uninitialized
+/// memory. The removed elements stay reachable from the buffer until those
+/// slots are reused by later pushes, until the buffer grows, or until the
+/// array is dropped; `Array::release_unused` overwrites them on demand.
fn[T] Array::unsafe_truncate_to_length(self : Array[T], new_len : Int) -> Unit {
- let len = self.length()
- guard! new_len <= len
- for i in new_len.. Unit {
let old_buf = self.buf
- let old_cap = old_buf.0.length()
- let copy_len = if old_cap < new_capacity { old_cap } else { new_capacity }
+ // Only the live prefix is worth carrying over. The slots beyond `len` hold
+ // either NULL or the elements earlier removals left behind, and leaving them
+ // with the old buffer is what releases the latter when that buffer dies.
+ let len = self.len
+ let copy_len = if len < new_capacity { len } else { new_capacity }
let new_buf = UninitializedArray::make_and_blit(
old_buf,
allocate_len=new_capacity,
@@ -274,7 +282,7 @@ test "UninitializedArray::unsafe_blit_fixed" {
///|
test "Array::resize_buffer" {
- let arr = Array::new(capacity=2)
+ let arr = Array::Array(capacity=2)
arr.push(1)
arr.push(2)
arr.resize_buffer(4)
@@ -325,7 +333,7 @@ pub fn[T] Array::reserve_capacity(self : Array[T], capacity : Int) -> Unit {
///
/// ```mbt check
/// test {
-/// let v = Array::new(capacity=10)
+/// let v = Array(capacity=10)
/// v.push(1)
/// v.push(2)
/// v.push(3)
@@ -333,6 +341,12 @@ pub fn[T] Array::reserve_capacity(self : Array[T], capacity : Int) -> Unit {
/// @test.assert_eq(v.capacity(), 3)
/// }
/// ```
+///
+/// The survivors are copied into the new buffer and the old one is released
+/// with them, so this also releases whatever earlier removals left in the
+/// unused capacity. It pays an allocation plus a copy of every survivor to do
+/// so; `Array::release_unused` releases the same elements in one pass over the
+/// unused region and no allocation, at the cost of leaving the capacity alone.
pub fn[T] Array::shrink_to_fit(self : Array[T]) -> Unit {
if self.capacity() <= self.length() {
return
@@ -340,6 +354,43 @@ pub fn[T] Array::shrink_to_fit(self : Array[T]) -> Unit {
self.resize_buffer(self.length())
}
+///|
+/// Overwrites the array's unused capacity -- every slot from `length()` up to
+/// `capacity()` -- with `placeholder`, releasing whatever those slots held.
+///
+/// Shrinking an array never clears the slots it vacates, so whatever they held
+/// -- a removed element, or a duplicate reference to a survivor that was
+/// shifted over it -- stays reachable from the buffer and unreleased until
+/// later pushes reuse those slots, the buffer grows, or the array is dropped.
+/// This releases them on demand without reallocating, which is what
+/// `Array::pop`, `Array::remove`, `Array::retain` and the other operations
+/// that take no placeholder leave outstanding. `Array::shrink_to_fit` releases
+/// them too, but by allocating an exact-size buffer and copying every survivor
+/// into it; this costs one pass over the unused region and no allocation.
+///
+/// An `ArrayView` created before the call observes `placeholder` in that region
+/// afterwards, in place of whatever it held.
+///
+/// This only matters for element types holding references -- for types such as
+/// `Int` there is nothing to release and the call merely costs a pass over the
+/// buffer.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// let arr = ["a", "b", "c"]
+/// let _ = arr.pop()
+/// arr.release_unused(placeholder="")
+/// debug_inspect(arr, content="[\"a\", \"b\"]")
+/// }
+/// ```
+#owned(placeholder)
+pub fn[T] Array::release_unused(self : Array[T], placeholder~ : T) -> Unit {
+ let len = self.len
+ self.buf.unchecked_fill(len, placeholder, self.capacity() - len)
+}
+
///|
/// Adds an element to the end of the array.
///
@@ -525,6 +576,11 @@ pub fn[A] ArrayView::blit_to(
///|
/// Removes the last element from an array and returns it, or `None` if it is empty.
///
+/// The vacated slot goes on referring to the returned element, so an
+/// `ArrayView` created beforehand keeps observing that element, and it is
+/// released only once a later push reuses the slot, the buffer grows, or the
+/// buffer is dropped. Call `Array::release_unused` to release it at once.
+///
/// # Example
/// ```mbt check
/// test {
@@ -540,7 +596,8 @@ pub fn[T] Array::pop(self : Array[T]) -> T? {
} else {
let index = len - 1
let v = self.unsafe_get(index)
- self.buf.set_null(index)
+ // The slot keeps referring to `v` until it is reused or the buffer dies;
+ // see `unsafe_truncate_to_length` for why it is not nulled out.
self.len = index
Some(v)
}
@@ -555,6 +612,10 @@ pub fn[T] Array::pop(self : Array[T]) -> T? {
///
/// Returns the last element of the array before removal.
///
+/// As with `Array::pop`, the vacated slot goes on referring to the returned
+/// element until a later push reuses it, the buffer grows, the buffer is
+/// dropped, or `Array::release_unused` overwrites it.
+///
/// Example:
///
/// ```mbt check
@@ -573,7 +634,6 @@ pub fn[T] Array::unsafe_pop(self : Array[T]) -> T {
guard! len != 0
let index = len - 1
let v = self.unsafe_get(index)
- self.buf.set_null(index)
self.len = index
v
}
@@ -626,6 +686,14 @@ pub fn[T] Array::remove(self : Array[T], index : Int) -> T {
/// @test.assert_eq(v, [3, 5])
/// }
/// ```
+///
+/// The `end - begin` slots vacated at the end of the array are not cleared:
+/// each keeps whatever it held before the survivors were shifted down, so a
+/// drained element or a duplicate reference to a survivor stays reachable
+/// there until the slot is reused, the buffer grows, or the array is dropped.
+/// Call `Array::release_unused` to overwrite them at once, or
+/// `Array::shrink_to_fit` to move the survivors into an exact-size buffer.
+///
pub fn[T] Array::drain(self : Array[T], begin : Int, end : Int) -> Array[T] {
guard! begin >= 0 && end <= self.length() && begin <= end
let num = end - begin
@@ -775,10 +843,9 @@ pub fn[A] Array::fill(
pub fn[T] Array::copy(self : Array[T]) -> Array[T] {
let len = self.length()
if len == 0 {
- []
- } else {
- let arr = Array::make(len, self[0])
- Array::unsafe_blit(arr, 0, self, 0, len)
- arr
+ return []
}
+ let arr = Array::unsafe_make_uninit(len)
+ Array::unsafe_blit(arr, 0, self, 0, len)
+ arr
}
diff --git a/builtin/arrayview.mbt b/builtin/arrayview.mbt
index 9a5013e8ff..197098b11c 100644
--- a/builtin/arrayview.mbt
+++ b/builtin/arrayview.mbt
@@ -20,6 +20,25 @@
/// over a view keeps using those bounds even if the underlying array is later
/// structurally modified.
///
+/// Mutating an array while a view of it is alive is a program error. Because a
+/// view keeps its original bounds and does not track the array, after such a
+/// mutation it may observe elements that have since been removed, a value
+/// handed to `Array::release_unused`, or the contents of a buffer the array has
+/// stopped using. What it yields is always a valid value of `T` -- never
+/// uninitialized memory -- but is otherwise unspecified.
+///
+/// No removal writes to the slots it vacates, so the removed elements stay
+/// reachable, and so unreleased, until a later push reuses the slot, until the
+/// buffer grows, or until the buffer itself is dropped. That holds uniformly:
+/// `clear` empties an array the same way `pop` shortens it. Two operations
+/// release those elements on demand -- `Array::release_unused` overwrites the
+/// unused capacity in place, and `Array::shrink_to_fit` moves the survivors
+/// into an exact-size buffer and lets the old one go.
+///
+/// On the JavaScript backend this guarantee does not yet hold: operations such
+/// as `Array::pop` shrink the underlying JavaScript array, so a view reaching
+/// past the array's current length observes `undefined`.
+///
/// # Example
///
/// ```mbt check
@@ -166,6 +185,7 @@ pub fn[T] ArrayView::at(self : ArrayView[T], index : Int) -> T {
/// debug_inspect(view.get(5), content="None")
/// }
/// ```
+#intrinsic("%arrayview.get_opt")
pub fn[T] ArrayView::get(self : ArrayView[T], index : Int) -> T? {
let len = self.length()
guard index >= 0 && index < len else { None }
@@ -223,7 +243,7 @@ pub fn[T] ArrayView::unsafe_get(self : ArrayView[T], index : Int) -> T {
}
///|
-/// Creates a view of a portion of the array. The view provides read-write access
+/// Creates a view of a portion of the array. The view provides read-only access
/// to the underlying array without copying the elements.
///
/// Parameters:
@@ -249,6 +269,7 @@ pub fn[T] ArrayView::unsafe_get(self : ArrayView[T], index : Int) -> T {
/// inspect(view.length(), content="3") // View contains 3 elements
/// }
/// ```
+#intrinsic("%array.view")
#alias("_[_:_]")
#alias(sub, deprecated="Use _[_:_] instead")
pub fn[T] Array::view(
@@ -342,6 +363,7 @@ pub fn[T] Array::get_view(
/// inspect(subview[0], content="3")
/// }
/// ```
+#intrinsic("%arrayview.view")
#alias("_[_:_]")
#alias(sub, deprecated="Use _[_:_] instead")
pub fn[T] ArrayView::view(
@@ -439,6 +461,7 @@ fn[T] unsafe_cast_fixedarray_to_uninitializedarray(
/// inspect(view[0], content="2")
/// }
/// ```
+#intrinsic("%fixedarray.view")
#alias("_[_:_]")
#alias(sub, deprecated="Use _[_:_] instead")
pub fn[T] FixedArray::view(
@@ -727,7 +750,7 @@ pub fn[X] ArrayView::iter(self : ArrayView[X]) -> Iter[X] {
///|
/// Return a reverse iterator over elements of this view.
///
-/// Deprecated alias for reverse iteration; prefer `rev_iterator`.
+/// `rev_iterator` is a deprecated alias for this function.
///
/// Example:
///
@@ -1360,7 +1383,7 @@ pub fn[T, U] ArrayView::map(
self : ArrayView[T],
f : (T) -> U raise?,
) -> Array[U] raise? {
- let arr = Array::make_uninit(self.length())
+ let arr = Array::unsafe_make_uninit(self.length())
for i, x in self {
arr.unsafe_set(i, f(x))
}
@@ -1383,7 +1406,7 @@ pub fn[T, U] ArrayView::mapi(
self : ArrayView[T],
f : (Int, T) -> U raise?,
) -> Array[U] raise? {
- let arr = Array::make_uninit(self.length())
+ let arr = Array::unsafe_make_uninit(self.length())
for i, x in self {
arr.unsafe_set(i, f(i, x))
}
@@ -1580,7 +1603,7 @@ pub fn[T : Compare] ArrayView::lexical_compare(
/// ```
pub fn[T] ArrayView::rev(self : ArrayView[T]) -> Array[T] {
let len = self.length()
- let arr = Array::make_uninit(len)
+ let arr = Array::unsafe_make_uninit(len)
for i in 0..,
#| ,
- #| ,
- #| ,
+ #| ,
+ #| ,
#| ]>
),
)
@@ -505,11 +505,35 @@ test "arrayview_arbitrary" {
content=(
#|,
- #| ,
- #| ,
- #| ,
- #| ,
+ #| ,
+ #| ,
+ #| ,
+ #| ,
+ #| ,
#| ]>
),
)
@@ -520,10 +544,20 @@ test "arrayview_hash" {
let arr : Array[ArrayView[Int]] = @quickcheck.samples(20)
let h1 = Hasher(seed=0)
h1.combine(arr[5:9])
- inspect(h1.finalize(), content="-966877954")
+ inspect(
+ h1.finalize(),
+ content=(
+ #|-1804859757
+ ),
+ )
let h2 = Hasher(seed=0)
h2.combine(arr[10:15])
- inspect(h2.finalize(), content="-951019668")
+ inspect(
+ h2.finalize(),
+ content=(
+ #|-1122600380
+ ),
+ )
}
///|
diff --git a/builtin/autoloc.mbt b/builtin/autoloc.mbt
index dadc250ded..e14c7a48bf 100644
--- a/builtin/autoloc.mbt
+++ b/builtin/autoloc.mbt
@@ -31,8 +31,8 @@ pub(all) type SourceLoc
/// * `source_location` : A source code location containing information about the
/// file path, line number, and column number.
///
-/// Returns a string representation of the source location, typically in the
-/// format "@package:file:start_line:start_column-end_line:end_column".
+/// Returns a string representation of the source location, in the format
+/// "file:start_line:start_column-end_line:end_column@module".
///
/// Note: This function is primarily used internally by the compiler for error
/// reporting and debugging purposes. Source locations are automatically created
@@ -72,7 +72,7 @@ fn SourceLocRepr::parse(repr : String) -> SourceLocRepr {
(re"[[:digit:]]+" as end_column) +
re"@" +
(re"[^:]*$" as _module) =>
- { filename, start_line, start_column, end_line, end_column }
+ { filename, start_line, start_column, end_line, end_column, }
_ => panic()
}
}
@@ -106,15 +106,16 @@ pub impl Show for ArgsLoc with fn output(self, logger) {
///|
/// Converts an array of optional source locations to its JSON string
-/// representation. Each location in the array is either represented as a string
-/// if present, or "null" if absent.
+/// representation. Each location in the array is either represented as a JSON
+/// object with `filename`, `start_line`, `start_column`, `end_line` and
+/// `end_column` fields if present, or "null" if absent.
///
/// Parameters:
///
/// * `self` : The array of optional source locations to be converted.
///
-/// Returns a JSON array string where each element is either a string
-/// representation of a source location or "null".
+/// Returns a JSON array string where each element is either a JSON object
+/// describing a source location or "null".
pub fn ArgsLoc::to_json(self : ArgsLoc) -> String {
let buf = StringBuilder(size_hint=10)
let ArgsLoc(self) = self
diff --git a/builtin/bitstring.mbt b/builtin/bitstring.mbt
index c0f1f5adf7..370c87f496 100644
--- a/builtin/bitstring.mbt
+++ b/builtin/bitstring.mbt
@@ -107,9 +107,27 @@ pub fn ArrayView::unsafe_extract_uint_le(
bs : ArrayView[Byte],
offset : Int,
len : Int,
+) -> UInt {
+ if (offset & 7) == 0 {
+ if len == 32 {
+ return bs.unsafe_extract_uint_le_aligned(offset >> 3)
+ }
+ if len == 16 {
+ return bs.unsafe_extract_uint16_le_aligned(offset >> 3)
+ }
+ }
+ bs.unsafe_extract_uint_le_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint_le`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn ArrayView::unsafe_extract_uint_le_slow(
+ bs : ArrayView[Byte],
+ offset : Int,
+ len : Int,
) -> UInt {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8)
match bytes_needed {
@@ -145,9 +163,27 @@ pub fn ArrayView::unsafe_extract_uint_be(
bs : ArrayView[Byte],
offset : Int,
len : Int,
+) -> UInt {
+ if (offset & 7) == 0 {
+ if len == 32 {
+ return bs.unsafe_extract_uint_be_aligned(offset >> 3)
+ }
+ if len == 16 {
+ return bs.unsafe_extract_uint16_be_aligned(offset >> 3)
+ }
+ }
+ bs.unsafe_extract_uint_be_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint_be`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn ArrayView::unsafe_extract_uint_be_slow(
+ bs : ArrayView[Byte],
+ offset : Int,
+ len : Int,
) -> UInt {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8)
match bytes_needed {
@@ -182,7 +218,7 @@ pub fn ArrayView::unsafe_extract_uint_be(
/// # Invariants
/// - It's guaranteed to have at least 5 bytes available for extraction
/// - Only reads the necessary number of bytes based on the bit length (5-8 bytes)
-/// - For bit lengths < 33, use unsafe_extract_int_le instead
+/// - For bit lengths [9..32], use unsafe_extract_uint_le instead
///
#internal(experimental, "subject to breaking change without notice")
#doc(hidden)
@@ -190,9 +226,22 @@ pub fn ArrayView::unsafe_extract_uint64_le(
bs : ArrayView[Byte],
offset : Int,
len : Int,
+) -> UInt64 {
+ if len == 64 && (offset & 7) == 0 {
+ return bs.unsafe_extract_uint64_le_aligned(offset >> 3)
+ }
+ bs.unsafe_extract_uint64_le_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint64_le`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn ArrayView::unsafe_extract_uint64_le_slow(
+ bs : ArrayView[Byte],
+ offset : Int,
+ len : Int,
) -> UInt64 {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8).to_uint64()
let b1 = bs.unsafe_extract_byte(offset + 8, 8).to_uint64()
@@ -244,7 +293,7 @@ pub fn ArrayView::unsafe_extract_uint64_le(
/// # Invariants
/// - It's guaranteed to have at least 5 bytes available for extraction
/// - Only reads the necessary number of bytes based on the bit length (5-8 bytes)
-/// - For bit lengths < 33, use unsafe_extract_int_be instead
+/// - For bit lengths [9..32], use unsafe_extract_uint_be instead
///
#internal(experimental, "subject to breaking change without notice")
#doc(hidden)
@@ -252,9 +301,22 @@ pub fn ArrayView::unsafe_extract_uint64_be(
bs : ArrayView[Byte],
offset : Int,
len : Int,
+) -> UInt64 {
+ if len == 64 && (offset & 7) == 0 {
+ return bs.unsafe_extract_uint64_be_aligned(offset >> 3)
+ }
+ bs.unsafe_extract_uint64_be_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint64_be`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn ArrayView::unsafe_extract_uint64_be_slow(
+ bs : ArrayView[Byte],
+ offset : Int,
+ len : Int,
) -> UInt64 {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8).to_uint64()
let b1 = bs.unsafe_extract_byte(offset + 8, 8).to_uint64()
@@ -983,9 +1045,27 @@ pub fn BytesView::unsafe_extract_uint_le(
bs : BytesView,
offset : Int,
len : Int,
+) -> UInt {
+ if (offset & 7) == 0 {
+ if len == 32 {
+ return bs.unsafe_extract_uint_le_aligned(offset >> 3)
+ }
+ if len == 16 {
+ return bs.unsafe_extract_uint16_le_aligned(offset >> 3)
+ }
+ }
+ bs.unsafe_extract_uint_le_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint_le`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn BytesView::unsafe_extract_uint_le_slow(
+ bs : BytesView,
+ offset : Int,
+ len : Int,
) -> UInt {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8)
match bytes_needed {
@@ -1021,9 +1101,27 @@ pub fn BytesView::unsafe_extract_uint_be(
bs : BytesView,
offset : Int,
len : Int,
+) -> UInt {
+ if (offset & 7) == 0 {
+ if len == 32 {
+ return bs.unsafe_extract_uint_be_aligned(offset >> 3)
+ }
+ if len == 16 {
+ return bs.unsafe_extract_uint16_be_aligned(offset >> 3)
+ }
+ }
+ bs.unsafe_extract_uint_be_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint_be`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn BytesView::unsafe_extract_uint_be_slow(
+ bs : BytesView,
+ offset : Int,
+ len : Int,
) -> UInt {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8)
match bytes_needed {
@@ -1058,7 +1156,7 @@ pub fn BytesView::unsafe_extract_uint_be(
/// # Invariants
/// - It's guaranteed to have at least 5 bytes available for extraction
/// - Only reads the necessary number of bytes based on the bit length (5-8 bytes)
-/// - For bit lengths < 33, use unsafe_extract_int_le instead
+/// - For bit lengths [9..32], use unsafe_extract_uint_le instead
///
#internal(experimental, "subject to breaking change without notice")
#doc(hidden)
@@ -1066,9 +1164,22 @@ pub fn BytesView::unsafe_extract_uint64_le(
bs : BytesView,
offset : Int,
len : Int,
+) -> UInt64 {
+ if len == 64 && (offset & 7) == 0 {
+ return bs.unsafe_extract_uint64_le_aligned(offset >> 3)
+ }
+ bs.unsafe_extract_uint64_le_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint64_le`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn BytesView::unsafe_extract_uint64_le_slow(
+ bs : BytesView,
+ offset : Int,
+ len : Int,
) -> UInt64 {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8).to_uint64()
let b1 = bs.unsafe_extract_byte(offset + 8, 8).to_uint64()
@@ -1120,7 +1231,7 @@ pub fn BytesView::unsafe_extract_uint64_le(
/// # Invariants
/// - It's guaranteed to have at least 5 bytes available for extraction
/// - Only reads the necessary number of bytes based on the bit length (5-8 bytes)
-/// - For bit lengths < 33, use unsafe_extract_int_be instead
+/// - For bit lengths [9..32], use unsafe_extract_uint_be instead
///
#internal(experimental, "subject to breaking change without notice")
#doc(hidden)
@@ -1128,9 +1239,22 @@ pub fn BytesView::unsafe_extract_uint64_be(
bs : BytesView,
offset : Int,
len : Int,
+) -> UInt64 {
+ if len == 64 && (offset & 7) == 0 {
+ return bs.unsafe_extract_uint64_be_aligned(offset >> 3)
+ }
+ bs.unsafe_extract_uint64_be_slow(offset, len)
+}
+
+///|
+/// Outlined slow path of `unsafe_extract_uint64_be`, kept out of the entry
+/// function so the aligned fast paths above stay small enough to inline.
+fn BytesView::unsafe_extract_uint64_be_slow(
+ bs : BytesView,
+ offset : Int,
+ len : Int,
) -> UInt64 {
let bytes_needed = (len + 7) / 8
- // TODO: add fast path for aligned case
// non-aligned case: extract bytes using unsafe_extract_byte
let b0 = bs.unsafe_extract_byte(offset, 8).to_uint64()
let b1 = bs.unsafe_extract_byte(offset + 8, 8).to_uint64()
diff --git a/builtin/bitstring_extract_bench_test.mbt b/builtin/bitstring_extract_bench_test.mbt
new file mode 100644
index 0000000000..362f8969e3
--- /dev/null
+++ b/builtin/bitstring_extract_bench_test.mbt
@@ -0,0 +1,192 @@
+// 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.
+
+// Benchmarks for the generic bitstring extractors (workload shape from
+// PR #3777 by @mizchi): the aligned exact-width fast paths (32/64-bit and
+// 16-bit), plus the outlined slow-path cases they must not pessimize —
+// unaligned offsets and aligned widths without a fixed-width helper
+// (e.g. 24-bit).
+
+///|
+let bitstring_extract_bench_size = 8192
+
+///|
+fn bitstring_extract_bench_bytes() -> Bytes {
+ Bytes::makei(bitstring_extract_bench_size, i => (i & 0xff).to_byte())
+}
+
+///|
+fn bitstring_extract_bench_array() -> Array[Byte] {
+ Array::makei(bitstring_extract_bench_size, i => (i & 0xff).to_byte())
+}
+
+///|
+test "bench BytesView uint_le aligned len=32" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 4
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 4 {
+ acc = acc ^ view.unsafe_extract_uint_le(byte_offset * 8, 32)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint_be aligned len=32" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 4
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 4 {
+ acc = acc ^ view.unsafe_extract_uint_be(byte_offset * 8, 32)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench ArrayView uint_le aligned len=32" (it : @bench.T) {
+ let arr = bitstring_extract_bench_array()
+ let view = arr[:]
+ let last = bitstring_extract_bench_size - 4
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 4 {
+ acc = acc ^ view.unsafe_extract_uint_le(byte_offset * 8, 32)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench ArrayView uint_be aligned len=32" (it : @bench.T) {
+ let arr = bitstring_extract_bench_array()
+ let view = arr[:]
+ let last = bitstring_extract_bench_size - 4
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 4 {
+ acc = acc ^ view.unsafe_extract_uint_be(byte_offset * 8, 32)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint64_le aligned len=64" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 8
+ it.bench(fn() {
+ let mut acc : UInt64 = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 8 {
+ acc = acc ^ view.unsafe_extract_uint64_le(byte_offset * 8, 64)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint64_be aligned len=64" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 8
+ it.bench(fn() {
+ let mut acc : UInt64 = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 8 {
+ acc = acc ^ view.unsafe_extract_uint64_be(byte_offset * 8, 64)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench ArrayView uint64_le aligned len=64" (it : @bench.T) {
+ let arr = bitstring_extract_bench_array()
+ let view = arr[:]
+ let last = bitstring_extract_bench_size - 8
+ it.bench(fn() {
+ let mut acc : UInt64 = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 8 {
+ acc = acc ^ view.unsafe_extract_uint64_le(byte_offset * 8, 64)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench ArrayView uint64_be aligned len=64" (it : @bench.T) {
+ let arr = bitstring_extract_bench_array()
+ let view = arr[:]
+ let last = bitstring_extract_bench_size - 8
+ it.bench(fn() {
+ let mut acc : UInt64 = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 8 {
+ acc = acc ^ view.unsafe_extract_uint64_be(byte_offset * 8, 64)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint_le unaligned len=32" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 5
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 4 {
+ acc = acc ^ view.unsafe_extract_uint_le(byte_offset * 8 + 4, 32)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint64_le unaligned len=64" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 9
+ it.bench(fn() {
+ let mut acc : UInt64 = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 8 {
+ acc = acc ^ view.unsafe_extract_uint64_le(byte_offset * 8 + 4, 64)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint_le aligned len=16" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 2
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 2 {
+ acc = acc ^ view.unsafe_extract_uint_le(byte_offset * 8, 16)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench BytesView uint_le aligned len=24" (it : @bench.T) {
+ let view = bitstring_extract_bench_bytes()[:]
+ let last = bitstring_extract_bench_size - 3
+ it.bench(fn() {
+ let mut acc : UInt = 0
+ for byte_offset = 0; byte_offset <= last; byte_offset = byte_offset + 3 {
+ acc = acc ^ view.unsafe_extract_uint_le(byte_offset * 8, 24)
+ }
+ it.keep(acc)
+ })
+}
diff --git a/builtin/bitstring_extract_equiv_test.mbt b/builtin/bitstring_extract_equiv_test.mbt
new file mode 100644
index 0000000000..90df7d3dd5
--- /dev/null
+++ b/builtin/bitstring_extract_equiv_test.mbt
@@ -0,0 +1,111 @@
+// 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.
+
+// Equivalence sweep for the generic bitstring integer extractors: every
+// (offset, len) combination — aligned and unaligned, exact-width and
+// partial — is checked against a byte-composition reference built from
+// `unsafe_extract_byte`, so the aligned fast paths and the outlined slow
+// paths must agree with the specified bit semantics everywhere.
+
+///|
+fn extract_ref_le(read : (Int, Int) -> UInt, offset : Int, len : Int) -> UInt64 {
+ let n = (len + 7) / 8
+ let mut acc : UInt64 = 0
+ for k in 0.. UInt, offset : Int, len : Int) -> UInt64 {
+ let n = (len + 7) / 8
+ let mut acc : UInt64 = 0
+ for k in 0.. ((i * 37 + 11) & 0xff).to_byte())
+ let arr = Array::makei(32, i => ((i * 37 + 11) & 0xff).to_byte())
+ // both zero-origin views and a nonzero-origin subview, so the aligned
+ // helpers' start-offset handling is exercised too
+ let bv = bytes[:]
+ let av = arr[:]
+ let bv2 = bytes[3:]
+ let av2 = arr[3:]
+ let bv_read = (o : Int, l : Int) => bv.unsafe_extract_byte(o, l)
+ let av_read = (o : Int, l : Int) => av.unsafe_extract_byte(o, l)
+ let bv2_read = (o : Int, l : Int) => bv2.unsafe_extract_byte(o, l)
+ let av2_read = (o : Int, l : Int) => av2.unsafe_extract_byte(o, l)
+ for offset in 0..<=16 {
+ for len in 9..<=32 {
+ assert_eq(
+ bv.unsafe_extract_uint_le(offset, len).to_uint64(),
+ extract_ref_le(bv_read, offset, len),
+ )
+ assert_eq(
+ bv.unsafe_extract_uint_be(offset, len).to_uint64(),
+ extract_ref_be(bv_read, offset, len),
+ )
+ assert_eq(
+ av.unsafe_extract_uint_le(offset, len).to_uint64(),
+ extract_ref_le(av_read, offset, len),
+ )
+ assert_eq(
+ av.unsafe_extract_uint_be(offset, len).to_uint64(),
+ extract_ref_be(av_read, offset, len),
+ )
+ assert_eq(
+ bv2.unsafe_extract_uint_le(offset, len).to_uint64(),
+ extract_ref_le(bv2_read, offset, len),
+ )
+ assert_eq(
+ av2.unsafe_extract_uint_be(offset, len).to_uint64(),
+ extract_ref_be(av2_read, offset, len),
+ )
+ }
+ for len in 33..<=64 {
+ assert_eq(
+ bv.unsafe_extract_uint64_le(offset, len),
+ extract_ref_le(bv_read, offset, len),
+ )
+ assert_eq(
+ bv.unsafe_extract_uint64_be(offset, len),
+ extract_ref_be(bv_read, offset, len),
+ )
+ assert_eq(
+ av.unsafe_extract_uint64_le(offset, len),
+ extract_ref_le(av_read, offset, len),
+ )
+ assert_eq(
+ av.unsafe_extract_uint64_be(offset, len),
+ extract_ref_be(av_read, offset, len),
+ )
+ assert_eq(
+ bv2.unsafe_extract_uint64_le(offset, len),
+ extract_ref_le(bv2_read, offset, len),
+ )
+ assert_eq(
+ av2.unsafe_extract_uint64_be(offset, len),
+ extract_ref_be(av2_read, offset, len),
+ )
+ }
+ }
+}
diff --git a/builtin/bytes.mbt b/builtin/bytes.mbt
index 2acb1a3a18..5a6fa86a6b 100644
--- a/builtin/bytes.mbt
+++ b/builtin/bytes.mbt
@@ -149,7 +149,7 @@ pub fn FixedArray::blit_from_string(
fn unsafe_from_bytes(bytes : Bytes) -> FixedArray[Byte] = "%identity"
///|
-/// Copy `length` chars from byte sequence `src`, starting at `src_offset`,
+/// Copy `length` bytes from byte sequence `src`, starting at `src_offset`,
/// into byte sequence `self`, starting at `bytes_offset`.
pub fn FixedArray::blit_from_bytes(
self : FixedArray[Byte],
@@ -396,16 +396,7 @@ pub impl Compare for Bytes with fn compare(self, other) {
if cmp != 0 {
return cmp
}
- for i in 0.. Bytes {
///
/// Parameters:
///
-/// * `array` : A fixed-size array of bytes to be converted into a bytes
+/// * `arr` : A fixed-size array of bytes to be converted into a bytes
/// sequence.
-/// * `length` : (Optional) The length of the resulting bytes sequence. If not
+/// * `len` : (Optional) The length of the resulting bytes sequence. If not
/// provided, uses the full length of the input array.
///
/// Returns a new bytes sequence containing the bytes from the input array. If a
@@ -596,7 +587,7 @@ pub fn BytesView::to_fixedarray(self : BytesView) -> FixedArray[Byte] {
///
/// ```mbt check
/// test {
-/// let iter = Iter::singleton(b'h')
+/// let iter = [|b'h'|]
/// let bytes = Bytes::from_iter(iter)
/// inspect(
/// bytes,
@@ -767,17 +758,18 @@ pub fn Bytes::is_empty(self : Bytes) -> Bool {
}
///|
-/// Retrieves a byte from the view at the specified index.
+/// Retrieves a byte from the byte sequence at the specified index.
///
/// Parameters:
///
-/// * `self` : The bytes view to retrieve the byte from.
-/// * `index` : The position in the view from which to retrieve the byte.
+/// * `self` : The byte sequence to retrieve the byte from.
+/// * `index` : The position in the byte sequence from which to retrieve the
+/// byte.
///
/// Returns the byte at the specified index, or None if the index is out of bounds.
///
/// Example:
-///
+///
/// ```mbt check
/// test {
/// let bytes = b"\x01\x02\x03"
@@ -793,6 +785,7 @@ pub fn Bytes::is_empty(self : Bytes) -> Bool {
/// debug_inspect(byte, content="None")
/// }
/// ```
+#intrinsic("%bytes.get_opt")
pub fn Bytes::get(self : Bytes, index : Int) -> Byte? {
guard index >= 0 && index < self.length() else { None }
Some(self[index])
@@ -805,8 +798,39 @@ pub fn Bytes::get(self : Bytes, index : Int) -> Byte? {
///
/// * `self` : The first bytes sequence.
/// * `other` : The second bytes sequence.
-/// TODO: marked as intrinsic, inline if it is constant
+///
+/// Returns a new bytes sequence containing the bytes of `self` followed by the
+/// bytes of `other`.
+// TODO: marked as intrinsic, inline if it is constant
pub impl Add for Bytes with fn add(self : Bytes, other : Bytes) -> Bytes {
+ self.bytes_add_impl(other)
+}
+
+///|
+#cfg(not(target="js"))
+fn Bytes::bytes_add_impl(self : Bytes, other : Bytes) -> Bytes {
+ let len_self = self.length()
+ let len_other = other.length()
+ let rv = UninitializedArray::unsafe_make_and_blit_from_fixed(
+ unsafe_from_bytes(self),
+ len_self + len_other,
+ 0,
+ 0,
+ len_self,
+ )
+ UninitializedArray::unsafe_blit_fixed(
+ rv,
+ len_self,
+ unsafe_from_bytes(other),
+ 0,
+ len_other,
+ )
+ unsafe_to_bytes(buffer_to_fixedarray(rv))
+}
+
+///|
+#cfg(target="js")
+fn Bytes::bytes_add_impl(self : Bytes, other : Bytes) -> Bytes {
let len_self = self.length()
let len_other = other.length()
let rv : FixedArray[Byte] = FixedArray::make(len_self + len_other, 0)
diff --git a/builtin/bytes_add_bench_test.mbt b/builtin/bytes_add_bench_test.mbt
new file mode 100644
index 0000000000..5fe4900847
--- /dev/null
+++ b/builtin/bytes_add_bench_test.mbt
@@ -0,0 +1,20 @@
+// 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.
+
+///|
+test "bench Bytes add total=1000000" (it : @bench.T) {
+ let bytes_add_left : Bytes = Bytes::make(500000, b'a')
+ let bytes_add_right : Bytes = Bytes::make(500000, b'b')
+ it.bench(fn() { it.keep(bytes_add_left + bytes_add_right) })
+}
diff --git a/builtin/bytes_output_bench_test.mbt b/builtin/bytes_output_bench_test.mbt
new file mode 100644
index 0000000000..9bfea5bb8e
--- /dev/null
+++ b/builtin/bytes_output_bench_test.mbt
@@ -0,0 +1,26 @@
+// 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.
+
+///|
+let bytes_output_binary : Bytes = Bytes::makei(100000, i => i.to_byte())
+
+///|
+test "bench Bytes Show binary n=100000" (it : @bench.T) {
+ it.bench(fn() { it.keep(bytes_output_binary.to_string()) })
+}
+
+///|
+test "bench Bytes ToJson binary n=100000" (it : @bench.T) {
+ it.bench(fn() { it.keep(@json.to_json(bytes_output_binary)) })
+}
diff --git a/builtin/bytes_test.mbt b/builtin/bytes_test.mbt
index 3d69042f4a..1d54f71394 100644
--- a/builtin/bytes_test.mbt
+++ b/builtin/bytes_test.mbt
@@ -26,6 +26,13 @@ test "json repr" {
)
}
+///|
+test "Bytes output writes lowercase hex digits directly" {
+ let bytes = b"\x00\x09\x0a\x0f\x10\x7f\x80\xff"
+ inspect(bytes, content="b\"\\x00\\x09\\x0a\\x0f\\x10\\x7f\\x80\\xff\"")
+ @json.json_inspect(bytes, content="\\x00\\x09\\x0a\\x0f\\x10\\x7f\\x80\\xff")
+}
+
///|
test "to_string" {
@test.assert_eq(HELLO_WORLD.to_unchecked_string(), "Hello, World!")
@@ -271,6 +278,19 @@ test "Bytes::add and repeat" {
)
}
+///|
+test "Bytes add initializes the complete result" {
+ @test.assert_eq(b"" + b"", b"")
+ @test.assert_eq(b"left" + b"", b"left")
+ @test.assert_eq(b"" + b"right", b"right")
+ let left = Bytes::makei(257, i => i.to_byte())
+ let right = Bytes::makei(259, i => (255 - i).to_byte())
+ let joined = left + right
+ @test.assert_eq(joined.length(), 516)
+ @test.assert_eq(joined[0:257].to_owned(), left)
+ @test.assert_eq(joined[257:].to_owned(), right)
+}
+
///|
test "panic Bytes::repeat negative" {
let _ = b"ab".repeat(-1)
@@ -416,6 +436,25 @@ test "BytesView::lexical_compare/basic" {
inspect(long_lower[:].lexical_compare(short_higher), content="-1")
}
+///|
+test "Bytes and BytesView Compare preserve shortlex ordering" {
+ let equal_left = Bytes::makei(64, i => i.to_byte())
+ let equal_right = Bytes::makei(64, i => i.to_byte())
+ inspect(equal_left.compare(equal_right), content="0")
+
+ let lower = Bytes::makei(64, i => if i == 63 { b'\x01' } else { b'\x00' })
+ let higher = Bytes::makei(64, i => if i == 63 { b'\x02' } else { b'\x00' })
+ inspect(lower.compare(higher), content="-1")
+ inspect(higher.compare(lower), content="1")
+
+ let padded_lower = b"x" + lower + b"y"
+ let padded_higher = b"z" + higher + b"w"
+ inspect(padded_lower[1:65].compare(padded_higher[1:65]), content="-1")
+
+ // Shortlex compares length before contents.
+ inspect(b"\xff".compare(b"\x00\x00"), content="-1")
+}
+
///|
test "Bytes find and rev_find" {
let target = b"abcabc"
diff --git a/builtin/bytes_unsafe.mbt b/builtin/bytes_unsafe.mbt
index 413852e8ea..da50be8214 100644
--- a/builtin/bytes_unsafe.mbt
+++ b/builtin/bytes_unsafe.mbt
@@ -43,9 +43,14 @@ pub fn FixedArray::unsafe_write_uint64_le(
index : Int,
value : UInt64,
) -> Unit {
- for i in 0..<=7 {
- bytes.unsafe_set(i + index, (value >> (8 * i)).to_byte())
- }
+ bytes.unsafe_set(index, value.to_byte())
+ bytes.unsafe_set(index + 1, (value >> 8).to_byte())
+ bytes.unsafe_set(index + 2, (value >> 16).to_byte())
+ bytes.unsafe_set(index + 3, (value >> 24).to_byte())
+ bytes.unsafe_set(index + 4, (value >> 32).to_byte())
+ bytes.unsafe_set(index + 5, (value >> 40).to_byte())
+ bytes.unsafe_set(index + 6, (value >> 48).to_byte())
+ bytes.unsafe_set(index + 7, (value >> 56).to_byte())
}
///|
@@ -77,9 +82,14 @@ pub fn FixedArray::unsafe_write_uint64_be(
index : Int,
value : UInt64,
) -> Unit {
- for i in 0..<=7 {
- bytes.unsafe_set(i + index, (value >> (8 * (7 - i))).to_byte())
- }
+ bytes.unsafe_set(index, (value >> 56).to_byte())
+ bytes.unsafe_set(index + 1, (value >> 48).to_byte())
+ bytes.unsafe_set(index + 2, (value >> 40).to_byte())
+ bytes.unsafe_set(index + 3, (value >> 32).to_byte())
+ bytes.unsafe_set(index + 4, (value >> 24).to_byte())
+ bytes.unsafe_set(index + 5, (value >> 16).to_byte())
+ bytes.unsafe_set(index + 6, (value >> 8).to_byte())
+ bytes.unsafe_set(index + 7, value.to_byte())
}
///|
@@ -111,9 +121,10 @@ pub fn FixedArray::unsafe_write_uint32_le(
index : Int,
value : UInt,
) -> Unit {
- for i in 0..<=3 {
- bytes.unsafe_set(i + index, (value >> (8 * i)).to_byte())
- }
+ bytes.unsafe_set(index, value.to_byte())
+ bytes.unsafe_set(index + 1, (value >> 8).to_byte())
+ bytes.unsafe_set(index + 2, (value >> 16).to_byte())
+ bytes.unsafe_set(index + 3, (value >> 24).to_byte())
}
///|
@@ -145,9 +156,10 @@ pub fn FixedArray::unsafe_write_uint32_be(
index : Int,
value : UInt,
) -> Unit {
- for i in 0..<=3 {
- bytes.unsafe_set(i + index, (value >> (8 * (3 - i))).to_byte())
- }
+ bytes.unsafe_set(index, (value >> 24).to_byte())
+ bytes.unsafe_set(index + 1, (value >> 16).to_byte())
+ bytes.unsafe_set(index + 2, (value >> 8).to_byte())
+ bytes.unsafe_set(index + 3, value.to_byte())
}
///|
@@ -177,9 +189,8 @@ pub fn FixedArray::unsafe_write_uint16_le(
index : Int,
value : UInt16,
) -> Unit {
- for i in 0..<=1 {
- bytes.unsafe_set(i + index, (value >> (8 * i)).to_byte())
- }
+ bytes.unsafe_set(index, value.to_byte())
+ bytes.unsafe_set(index + 1, (value >> 8).to_byte())
}
///|
@@ -209,9 +220,8 @@ pub fn FixedArray::unsafe_write_uint16_be(
index : Int,
value : UInt16,
) -> Unit {
- for i in 0..<=1 {
- bytes.unsafe_set(i + index, (value >> (8 * (1 - i))).to_byte())
- }
+ bytes.unsafe_set(index, (value >> 8).to_byte())
+ bytes.unsafe_set(index + 1, value.to_byte())
}
// #endregion
@@ -242,11 +252,14 @@ pub fn FixedArray::unsafe_write_uint16_be(
#intrinsic("%bytes.unsafe_read_uint64_le")
#doc(hidden)
pub fn Bytes::unsafe_read_uint64_le(bytes : Bytes, index : Int) -> UInt64 {
- for i in 0..<=7; result = (0 : UInt64) {
- continue result | (bytes.unsafe_get(i + index).to_uint64() << (8 * i))
- } nobreak {
- result
- }
+ bytes.unsafe_get(index).to_uint64() |
+ (bytes.unsafe_get(index + 1).to_uint64() << 8) |
+ (bytes.unsafe_get(index + 2).to_uint64() << 16) |
+ (bytes.unsafe_get(index + 3).to_uint64() << 24) |
+ (bytes.unsafe_get(index + 4).to_uint64() << 32) |
+ (bytes.unsafe_get(index + 5).to_uint64() << 40) |
+ (bytes.unsafe_get(index + 6).to_uint64() << 48) |
+ (bytes.unsafe_get(index + 7).to_uint64() << 56)
}
///|
@@ -274,11 +287,14 @@ pub fn Bytes::unsafe_read_uint64_le(bytes : Bytes, index : Int) -> UInt64 {
#intrinsic("%bytes.unsafe_read_uint64_be")
#doc(hidden)
pub fn Bytes::unsafe_read_uint64_be(bytes : Bytes, index : Int) -> UInt64 {
- for i in 0..<=7; result = (0 : UInt64) {
- continue result | (bytes.unsafe_get(i + index).to_uint64() << (8 * (7 - i)))
- } nobreak {
- result
- }
+ (bytes.unsafe_get(index).to_uint64() << 56) |
+ (bytes.unsafe_get(index + 1).to_uint64() << 48) |
+ (bytes.unsafe_get(index + 2).to_uint64() << 40) |
+ (bytes.unsafe_get(index + 3).to_uint64() << 32) |
+ (bytes.unsafe_get(index + 4).to_uint64() << 24) |
+ (bytes.unsafe_get(index + 5).to_uint64() << 16) |
+ (bytes.unsafe_get(index + 6).to_uint64() << 8) |
+ bytes.unsafe_get(index + 7).to_uint64()
}
///|
@@ -306,11 +322,10 @@ pub fn Bytes::unsafe_read_uint64_be(bytes : Bytes, index : Int) -> UInt64 {
#intrinsic("%bytes.unsafe_read_uint32_le")
#doc(hidden)
pub fn Bytes::unsafe_read_uint32_le(bytes : Bytes, index : Int) -> UInt {
- for i in 0..<=3; result = (0 : UInt) {
- continue result | (bytes.unsafe_get(i + index).to_uint() << (8 * i))
- } nobreak {
- result
- }
+ bytes.unsafe_get(index).to_uint() |
+ (bytes.unsafe_get(index + 1).to_uint() << 8) |
+ (bytes.unsafe_get(index + 2).to_uint() << 16) |
+ (bytes.unsafe_get(index + 3).to_uint() << 24)
}
///|
@@ -338,11 +353,10 @@ pub fn Bytes::unsafe_read_uint32_le(bytes : Bytes, index : Int) -> UInt {
#intrinsic("%bytes.unsafe_read_uint32_be")
#doc(hidden)
pub fn Bytes::unsafe_read_uint32_be(bytes : Bytes, index : Int) -> UInt {
- for i in 0..<=3; result = (0 : UInt) {
- continue result | (bytes.unsafe_get(i + index).to_uint() << (8 * (3 - i)))
- } nobreak {
- result
- }
+ (bytes.unsafe_get(index).to_uint() << 24) |
+ (bytes.unsafe_get(index + 1).to_uint() << 16) |
+ (bytes.unsafe_get(index + 2).to_uint() << 8) |
+ bytes.unsafe_get(index + 3).to_uint()
}
///|
@@ -368,11 +382,8 @@ pub fn Bytes::unsafe_read_uint32_be(bytes : Bytes, index : Int) -> UInt {
#intrinsic("%bytes.unsafe_read_uint16_le")
#doc(hidden)
pub fn Bytes::unsafe_read_uint16_le(bytes : Bytes, index : Int) -> UInt16 {
- for i in 0..<=1; result = (0 : UInt16) {
- continue result | (bytes.unsafe_get(i + index).to_uint16() << (8 * i))
- } nobreak {
- result
- }
+ bytes.unsafe_get(index).to_uint16() |
+ (bytes.unsafe_get(index + 1).to_uint16() << 8)
}
///|
@@ -398,11 +409,8 @@ pub fn Bytes::unsafe_read_uint16_le(bytes : Bytes, index : Int) -> UInt16 {
#intrinsic("%bytes.unsafe_read_uint16_be")
#doc(hidden)
pub fn Bytes::unsafe_read_uint16_be(bytes : Bytes, index : Int) -> UInt16 {
- for i in 0..<=1; result = (0 : UInt16) {
- continue result | (bytes.unsafe_get(i + index).to_uint16() << (8 * (1 - i)))
- } nobreak {
- result
- }
+ (bytes.unsafe_get(index).to_uint16() << 8) |
+ bytes.unsafe_get(index + 1).to_uint16()
}
///|
@@ -596,66 +604,64 @@ fn buffer_to_fixedarray(buf : UninitializedArray[Byte]) -> FixedArray[Byte] = "%
#borrow(bytes)
#intrinsic("%bytes.unsafe_read_uint16_le")
fn fixedarray_read_uint16_le(bytes : FixedArray[Byte], index : Int) -> UInt16 {
- for i in 0..<=1; result = (0 : UInt16) {
- continue result | (bytes.unsafe_get(i + index).to_uint16() << (8 * i))
- } nobreak {
- result
- }
+ bytes.unsafe_get(index).to_uint16() |
+ (bytes.unsafe_get(index + 1).to_uint16() << 8)
}
///|
#borrow(bytes)
#intrinsic("%bytes.unsafe_read_uint16_be")
fn fixedarray_read_uint16_be(bytes : FixedArray[Byte], index : Int) -> UInt16 {
- for i in 0..<=1; result = (0 : UInt16) {
- continue result | (bytes.unsafe_get(i + index).to_uint16() << (8 * (1 - i)))
- } nobreak {
- result
- }
+ (bytes.unsafe_get(index).to_uint16() << 8) |
+ bytes.unsafe_get(index + 1).to_uint16()
}
///|
#borrow(bytes)
#intrinsic("%bytes.unsafe_read_uint32_le")
fn fixedarray_read_uint32_le(bytes : FixedArray[Byte], index : Int) -> UInt {
- for i in 0..<=3; result = (0 : UInt) {
- continue result | (bytes.unsafe_get(i + index).to_uint() << (8 * i))
- } nobreak {
- result
- }
+ bytes.unsafe_get(index).to_uint() |
+ (bytes.unsafe_get(index + 1).to_uint() << 8) |
+ (bytes.unsafe_get(index + 2).to_uint() << 16) |
+ (bytes.unsafe_get(index + 3).to_uint() << 24)
}
///|
#borrow(bytes)
#intrinsic("%bytes.unsafe_read_uint32_be")
fn fixedarray_read_uint32_be(bytes : FixedArray[Byte], index : Int) -> UInt {
- for i in 0..<=3; result = (0 : UInt) {
- continue result | (bytes.unsafe_get(i + index).to_uint() << (8 * (3 - i)))
- } nobreak {
- result
- }
+ (bytes.unsafe_get(index).to_uint() << 24) |
+ (bytes.unsafe_get(index + 1).to_uint() << 16) |
+ (bytes.unsafe_get(index + 2).to_uint() << 8) |
+ bytes.unsafe_get(index + 3).to_uint()
}
///|
#borrow(bytes)
#intrinsic("%bytes.unsafe_read_uint64_le")
fn fixedarray_read_uint64_le(bytes : FixedArray[Byte], index : Int) -> UInt64 {
- for i in 0..<=7; result = (0 : UInt64) {
- continue result | (bytes.unsafe_get(i + index).to_uint64() << (8 * i))
- } nobreak {
- result
- }
+ bytes.unsafe_get(index).to_uint64() |
+ (bytes.unsafe_get(index + 1).to_uint64() << 8) |
+ (bytes.unsafe_get(index + 2).to_uint64() << 16) |
+ (bytes.unsafe_get(index + 3).to_uint64() << 24) |
+ (bytes.unsafe_get(index + 4).to_uint64() << 32) |
+ (bytes.unsafe_get(index + 5).to_uint64() << 40) |
+ (bytes.unsafe_get(index + 6).to_uint64() << 48) |
+ (bytes.unsafe_get(index + 7).to_uint64() << 56)
}
///|
#borrow(bytes)
#intrinsic("%bytes.unsafe_read_uint64_be")
fn fixedarray_read_uint64_be(bytes : FixedArray[Byte], index : Int) -> UInt64 {
- for i in 0..<=7; result = (0 : UInt64) {
- continue result | (bytes.unsafe_get(i + index).to_uint64() << (8 * (7 - i)))
- } nobreak {
- result
- }
+ (bytes.unsafe_get(index).to_uint64() << 56) |
+ (bytes.unsafe_get(index + 1).to_uint64() << 48) |
+ (bytes.unsafe_get(index + 2).to_uint64() << 40) |
+ (bytes.unsafe_get(index + 3).to_uint64() << 32) |
+ (bytes.unsafe_get(index + 4).to_uint64() << 24) |
+ (bytes.unsafe_get(index + 5).to_uint64() << 16) |
+ (bytes.unsafe_get(index + 6).to_uint64() << 8) |
+ bytes.unsafe_get(index + 7).to_uint64()
}
// #endregion
diff --git a/builtin/bytes_unsafe_bench_test.mbt b/builtin/bytes_unsafe_bench_test.mbt
new file mode 100644
index 0000000000..bcd977c891
--- /dev/null
+++ b/builtin/bytes_unsafe_bench_test.mbt
@@ -0,0 +1,97 @@
+// 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.
+
+// Benchmarks for the `#intrinsic` fallback bodies in `bytes_unsafe.mbt`.
+//
+// These are lowered at the call site on native, so these numbers are expected
+// to be flat there; on wasm-gc and js the fallback body is what actually runs.
+// Only the little-endian variants are measured — the big-endian ones differ
+// solely in the constant shift amounts and compile to the same shape.
+
+///|
+let bytes_unsafe_bench_len = 4096
+
+///|
+fn bytes_unsafe_bench_data() -> Bytes {
+ Bytes::makei(bytes_unsafe_bench_len, i => ((i * 37 + 11) & 0xff).to_byte())
+}
+
+///|
+test "bench Bytes::unsafe_read_uint16_le n=4096" (it : @bench.T) {
+ let data = bytes_unsafe_bench_data()
+ it.bench(fn() {
+ let mut acc = (0 : UInt16)
+ for i in 0..<(bytes_unsafe_bench_len - 1) {
+ acc = acc + data.unsafe_read_uint16_le(i)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench Bytes::unsafe_read_uint32_le n=4096" (it : @bench.T) {
+ let data = bytes_unsafe_bench_data()
+ it.bench(fn() {
+ let mut acc = 0U
+ for i in 0..<(bytes_unsafe_bench_len - 3) {
+ acc = acc + data.unsafe_read_uint32_le(i)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench Bytes::unsafe_read_uint64_le n=4096" (it : @bench.T) {
+ let data = bytes_unsafe_bench_data()
+ it.bench(fn() {
+ let mut acc = 0UL
+ for i in 0..<(bytes_unsafe_bench_len - 7) {
+ acc = acc + data.unsafe_read_uint64_le(i)
+ }
+ it.keep(acc)
+ })
+}
+
+///|
+test "bench FixedArray::unsafe_write_uint16_le n=4096" (it : @bench.T) {
+ let buf = FixedArray::make(bytes_unsafe_bench_len, b'\x00')
+ it.bench(fn() {
+ for i in 0..<(bytes_unsafe_bench_len - 1) {
+ buf.unsafe_write_uint16_le(i, (i & 0xffff).to_uint16())
+ }
+ it.keep(buf[0])
+ })
+}
+
+///|
+test "bench FixedArray::unsafe_write_uint32_le n=4096" (it : @bench.T) {
+ let buf = FixedArray::make(bytes_unsafe_bench_len, b'\x00')
+ it.bench(fn() {
+ for i in 0..<(bytes_unsafe_bench_len - 3) {
+ buf.unsafe_write_uint32_le(i, i.reinterpret_as_uint())
+ }
+ it.keep(buf[0])
+ })
+}
+
+///|
+test "bench FixedArray::unsafe_write_uint64_le n=4096" (it : @bench.T) {
+ let buf = FixedArray::make(bytes_unsafe_bench_len, b'\x00')
+ it.bench(fn() {
+ for i in 0..<(bytes_unsafe_bench_len - 7) {
+ buf.unsafe_write_uint64_le(i, i.to_uint64())
+ }
+ it.keep(buf[0])
+ })
+}
diff --git a/builtin/bytes_unsafe_unroll_wbtest.mbt b/builtin/bytes_unsafe_unroll_wbtest.mbt
new file mode 100644
index 0000000000..a0877d394b
--- /dev/null
+++ b/builtin/bytes_unsafe_unroll_wbtest.mbt
@@ -0,0 +1,212 @@
+// 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.
+
+// Equivalence tests for the unrolled `#intrinsic` fallback bodies in
+// `bytes_unsafe.mbt`. Each `ref_*` function below is the loop form those
+// bodies used to have, kept here as an independent oracle: the unrolled
+// implementations must agree with it at every offset, for every sample value.
+//
+// These matter because the fallbacks are dead code on native (the intrinsics
+// are lowered at the call site) but are the real implementation on wasm-gc and
+// js, so a byte-order slip would only surface on two of three backends.
+
+///|
+/// `builtin` cannot depend on `@test`, so `assert_eq` is not available here;
+/// `assert_true` is the only assertion in scope.
+fn[T : Eq] check(actual : T, expected : T) -> Unit raise {
+ assert_true(actual == expected)
+}
+
+///|
+fn ref_read_u16_le(b : Bytes, index : Int) -> UInt16 {
+ for i in 0..<=1; result = (0 : UInt16) {
+ continue result | (b[i + index].to_uint16() << (8 * i))
+ } nobreak {
+ result
+ }
+}
+
+///|
+fn ref_read_u16_be(b : Bytes, index : Int) -> UInt16 {
+ for i in 0..<=1; result = (0 : UInt16) {
+ continue result | (b[i + index].to_uint16() << (8 * (1 - i)))
+ } nobreak {
+ result
+ }
+}
+
+///|
+fn ref_read_u32_le(b : Bytes, index : Int) -> UInt {
+ for i in 0..<=3; result = (0 : UInt) {
+ continue result | (b[i + index].to_uint() << (8 * i))
+ } nobreak {
+ result
+ }
+}
+
+///|
+fn ref_read_u32_be(b : Bytes, index : Int) -> UInt {
+ for i in 0..<=3; result = (0 : UInt) {
+ continue result | (b[i + index].to_uint() << (8 * (3 - i)))
+ } nobreak {
+ result
+ }
+}
+
+///|
+fn ref_read_u64_le(b : Bytes, index : Int) -> UInt64 {
+ for i in 0..<=7; result = (0 : UInt64) {
+ continue result | (b[i + index].to_uint64() << (8 * i))
+ } nobreak {
+ result
+ }
+}
+
+///|
+fn ref_read_u64_be(b : Bytes, index : Int) -> UInt64 {
+ for i in 0..<=7; result = (0 : UInt64) {
+ continue result | (b[i + index].to_uint64() << (8 * (7 - i)))
+ } nobreak {
+ result
+ }
+}
+
+///|
+/// A byte pattern with every bit position exercised, plus 0x00 and 0xFF runs so
+/// sign-extension and zero-extension slips show up.
+fn sample_bytes(len : Int) -> Bytes {
+ Bytes::makei(len, i => {
+ match i % 8 {
+ 0 => b'\x00'
+ 1 => b'\xFF'
+ 2 => b'\x80'
+ 3 => b'\x01'
+ 4 => b'\x7F'
+ 5 => b'\xAA'
+ 6 => b'\x55'
+ _ => (i * 31 + 7).to_byte()
+ }
+ })
+}
+
+///|
+test "unrolled Bytes reads agree with the loop form at every offset" {
+ let len = 64
+ let b = sample_bytes(len)
+ for i in 0..<(len - 1) {
+ check(b.unsafe_read_uint16_le(i), ref_read_u16_le(b, i))
+ check(b.unsafe_read_uint16_be(i), ref_read_u16_be(b, i))
+ }
+ for i in 0..<(len - 3) {
+ check(b.unsafe_read_uint32_le(i), ref_read_u32_le(b, i))
+ check(b.unsafe_read_uint32_be(i), ref_read_u32_be(b, i))
+ }
+ for i in 0..<(len - 7) {
+ check(b.unsafe_read_uint64_le(i), ref_read_u64_le(b, i))
+ check(b.unsafe_read_uint64_be(i), ref_read_u64_be(b, i))
+ }
+}
+
+///|
+/// The private `fixedarray_read_*` helpers carry their own copies of the same
+/// unrolled bodies, so they need their own check.
+test "unrolled FixedArray reads agree with the loop form at every offset" {
+ let len = 64
+ let b = sample_bytes(len)
+ let fa = FixedArray::makei(len, i => b[i])
+ for i in 0..<(len - 1) {
+ check(fixedarray_read_uint16_le(fa, i), ref_read_u16_le(b, i))
+ check(fixedarray_read_uint16_be(fa, i), ref_read_u16_be(b, i))
+ }
+ for i in 0..<(len - 3) {
+ check(fixedarray_read_uint32_le(fa, i), ref_read_u32_le(b, i))
+ check(fixedarray_read_uint32_be(fa, i), ref_read_u32_be(b, i))
+ }
+ for i in 0..<(len - 7) {
+ check(fixedarray_read_uint64_le(fa, i), ref_read_u64_le(b, i))
+ check(fixedarray_read_uint64_be(fa, i), ref_read_u64_be(b, i))
+ }
+}
+
+///|
+let u64_samples : Array[UInt64] = [
+ 0, 1, 0xFF, 0x100, 0x7FFF_FFFF, 0x8000_0000, 0xFFFF_FFFF, 0x1_0000_0000, 0x0123_4567_89AB_CDEF,
+ 0xFEDC_BA98_7654_3210, 0x7FFF_FFFF_FFFF_FFFF, 0x8000_0000_0000_0000, 0xFFFF_FFFF_FFFF_FFFF,
+]
+
+///|
+/// Writes go through the unrolled `unsafe_write_*`; reading them back with the
+/// independent `ref_read_*` oracle pins the byte order in both directions.
+test "unrolled writes round-trip through the loop-form readers" {
+ let len = 32
+ for value in u64_samples {
+ let v32 = value.to_uint()
+ let v16 = value.to_uint16()
+ for i in 0..<(len - 7) {
+ let fa = FixedArray::make(len, b'\x00')
+ fa.unsafe_write_uint64_le(i, value)
+ check(ref_read_u64_le(unsafe_to_bytes(fa), i), value)
+ let fa = FixedArray::make(len, b'\x00')
+ fa.unsafe_write_uint64_be(i, value)
+ check(ref_read_u64_be(unsafe_to_bytes(fa), i), value)
+ }
+ for i in 0..<(len - 3) {
+ let fa = FixedArray::make(len, b'\x00')
+ fa.unsafe_write_uint32_le(i, v32)
+ check(ref_read_u32_le(unsafe_to_bytes(fa), i), v32)
+ let fa = FixedArray::make(len, b'\x00')
+ fa.unsafe_write_uint32_be(i, v32)
+ check(ref_read_u32_be(unsafe_to_bytes(fa), i), v32)
+ }
+ for i in 0..<(len - 1) {
+ let fa = FixedArray::make(len, b'\x00')
+ fa.unsafe_write_uint16_le(i, v16)
+ check(ref_read_u16_le(unsafe_to_bytes(fa), i), v16)
+ let fa = FixedArray::make(len, b'\x00')
+ fa.unsafe_write_uint16_be(i, v16)
+ check(ref_read_u16_be(unsafe_to_bytes(fa), i), v16)
+ }
+ }
+}
+
+///|
+/// A write must touch exactly its own bytes and leave the neighbours alone.
+/// All six writers are covered, since each carries its own unrolled body.
+test "unrolled writes do not disturb neighbouring bytes" {
+ let len = 32
+ let fill = b'\x3C'
+ fn check_span(
+ width : Int,
+ write : (FixedArray[Byte], Int) -> Unit,
+ ) -> Unit raise {
+ for i in 0..<(len - width + 1) {
+ let fa = FixedArray::make(len, fill)
+ write(fa, i)
+ for j in 0..= i + width {
+ check(fa[j], fill)
+ } else {
+ check(fa[j], b'\xFF')
+ }
+ }
+ }
+ }
+
+ check_span(8, (fa, i) => fa.unsafe_write_uint64_le(i, 0xFFFF_FFFF_FFFF_FFFF))
+ check_span(8, (fa, i) => fa.unsafe_write_uint64_be(i, 0xFFFF_FFFF_FFFF_FFFF))
+ check_span(4, (fa, i) => fa.unsafe_write_uint32_le(i, 0xFFFF_FFFF))
+ check_span(4, (fa, i) => fa.unsafe_write_uint32_be(i, 0xFFFF_FFFF))
+ check_span(2, (fa, i) => fa.unsafe_write_uint16_le(i, 0xFFFF))
+ check_span(2, (fa, i) => fa.unsafe_write_uint16_be(i, 0xFFFF))
+}
diff --git a/builtin/bytesview.mbt b/builtin/bytesview.mbt
index 7f8a8a07c3..cdb496fef3 100644
--- a/builtin/bytesview.mbt
+++ b/builtin/bytesview.mbt
@@ -72,7 +72,10 @@ pub fn BytesView::is_empty(self : BytesView) -> Bool {
/// * `self` : The bytes view to retrieve the byte from.
/// * `index` : The position in the view from which to retrieve the byte.
///
-/// Returns the byte at the specified index if the index is valid.
+/// Returns the byte at the specified index.
+///
+/// Throws a runtime error if the index is out of bounds (less than 0 or greater
+/// than or equal to the length of the view).
///
/// Example:
///
@@ -121,6 +124,7 @@ pub fn BytesView::at(self : BytesView, index : Int) -> Byte {
/// debug_inspect(result, content="None")
/// }
/// ```
+#intrinsic("%bytesview.get_opt")
pub fn BytesView::get(self : BytesView, index : Int) -> Byte? {
guard index >= 0 && index < self.length() else { None }
Some(self.bytes().unsafe_get(self.start() + index))
@@ -173,8 +177,9 @@ pub fn BytesView::unsafe_get(self : BytesView, index : Int) -> Byte {
/// @test.assert_eq(bv[2], b'\x03')
/// }
/// ```
+#intrinsic("%bytes.view")
#alias("_[_:_]")
-#alias(sub, deprecated="Use _[_:_ instead")
+#alias(sub, deprecated="Use _[_:_] instead")
pub fn Bytes::view(self : Bytes, start? : Int = 0, end? : Int) -> BytesView {
let len = self.length()
let end = match end {
@@ -246,6 +251,7 @@ pub fn BytesView::get_view(
/// @test.assert_eq(bv2[1], b'\x02')
/// }
/// ```
+#intrinsic("%bytesview.view")
#alias("_[_:_]")
#alias(sub, deprecated="Use _[_:_] instead")
pub fn BytesView::view(
@@ -325,286 +331,18 @@ pub fn BytesView::iter2(self : BytesView) -> Iter2[Int, Byte] {
}
///|
-/// Converts a 4-byte sequence to an unsigned 32-bit integer using big-endian
-/// byte order. The first byte is treated as the most significant byte, and the
-/// last byte as the least significant byte.
-///
-/// Parameters:
-///
-/// * `self` : A byte view containing exactly 4 bytes to be converted.
-///
-/// Returns an unsigned 32-bit integer representing the byte sequence.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x12\x34\x56\x78"
-/// guard! bytes is [u32be(x), ..]
-/// inspect(x, content="305419896") // 0x12345678
-/// }
-/// ```
-#deprecated("Use bits pattern directly")
-#doc(hidden)
-pub fn BytesView::to_uint_be(self : BytesView) -> UInt {
- (self[0].to_uint() << 24) +
- (self[1].to_uint() << 16) +
- (self[2].to_uint() << 8) +
- self[3].to_uint()
-}
-
-///|
-/// Converts a sequence of 4 bytes into an unsigned 32-bit integer using
-/// little-endian byte order. Each byte in the view contributes 8 bits to the
-/// final integer, with the least significant byte at index 0.
-///
-/// Parameters:
-///
-/// * `view` : A `View` containing exactly 4 bytes to be interpreted as a
-/// little-endian unsigned integer.
-///
-/// Returns an unsigned 32-bit integer (`UInt`) formed by interpreting the bytes
-/// in little-endian order.
-///
-/// Throws a panic if the view does not contain exactly 4 bytes.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x01\x02\x03\x04"
-/// guard! bytes is [u32le(x), ..]
-/// inspect(x, content="67305985") // 0x04030201
-/// }
-/// ```
-#deprecated("Use bits pattern directly")
-#doc(hidden)
-pub fn BytesView::to_uint_le(self : BytesView) -> UInt {
- self[0].to_uint() +
- (self[1].to_uint() << 8) +
- (self[2].to_uint() << 16) +
- (self[3].to_uint() << 24)
-}
-
-///|
-/// Converts a sequence of 8 bytes into a 64-bit unsigned integer using
-/// big-endian byte order. The most significant byte is at index 0, and the least
-/// significant byte is at index 7.
-///
-/// Parameters:
-///
-/// * `bytes` : A view into a byte sequence that must be at least 8 bytes long.
-/// The bytes are interpreted in big-endian order, where the first byte is the
-/// most significant byte.
-///
-/// Returns a 64-bit unsigned integer constructed by concatenating the bytes in
-/// big-endian order.
-///
-/// Throws a runtime error if the byte sequence view is less than 8 bytes long or
-/// if attempting to access an index beyond the view's bounds.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x01\x23\x45\x67\x89\xAB\xCD\xEF"
-/// guard! bytes is [u64be(x), ..]
-/// inspect(x, content="81985529216486895")
-/// }
-/// ```
-#deprecated("Use bits pattern directly")
-#doc(hidden)
-pub fn BytesView::to_uint64_be(self : BytesView) -> UInt64 {
- (self[0].to_uint().to_uint64() << 56) +
- (self[1].to_uint().to_uint64() << 48) +
- (self[2].to_uint().to_uint64() << 40) +
- (self[3].to_uint().to_uint64() << 32) +
- (self[4].to_uint().to_uint64() << 24) +
- (self[5].to_uint().to_uint64() << 16) +
- (self[6].to_uint().to_uint64() << 8) +
- self[7].to_uint().to_uint64()
-}
-
-///|
-/// Converts an 8-byte sequence to an unsigned 64-bit integer using little-endian
-/// byte order. Each byte in the view is treated as an 8-bit unsigned integer and
-/// combined to form the final 64-bit value, with the least significant byte
-/// first.
-///
-/// Parameters:
-///
-/// * `bytes_view` : A view into a byte sequence that must be exactly 8 bytes
-/// long. Each byte represents one byte of the resulting 64-bit integer, with the
-/// first byte being the least significant.
-///
-/// Returns an unsigned 64-bit integer assembled from the bytes in little-endian
-/// order.
-///
-/// Throws a panic if the View is less than 8 bytes long or if trying to
-/// access a byte beyond the view's bounds.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x01\x02\x03\x04\x05\x06\x07\x08"
-/// guard! bytes is [u64le(x), ..]
-/// inspect(x, content="578437695752307201")
-/// }
-/// ```
-#deprecated("Use bits pattern directly")
-#doc(hidden)
-pub fn BytesView::to_uint64_le(self : BytesView) -> UInt64 {
- self[0].to_uint().to_uint64() +
- (self[1].to_uint().to_uint64() << 8) +
- (self[2].to_uint().to_uint64() << 16) +
- (self[3].to_uint().to_uint64() << 24) +
- (self[4].to_uint().to_uint64() << 32) +
- (self[5].to_uint().to_uint64() << 40) +
- (self[6].to_uint().to_uint64() << 48) +
- (self[7].to_uint().to_uint64() << 56)
-}
-
-///|
-/// Interpret the first 4 bytes as a big-endian signed `Int`.
-///
-/// Deprecated: prefer bit-pattern matching (`u32be`) directly.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x00\x00\x00\x2A"
-/// guard! bytes is [u32be(u), ..]
-/// inspect(u.reinterpret_as_int(), content="42")
-/// }
-/// ```
-#deprecated
-#doc(hidden)
-pub fn BytesView::to_int_be(self : BytesView) -> Int {
- guard! self is [u32be(u32), ..]
- u32.reinterpret_as_int()
-}
-
-///|
-/// Interpret the first 4 bytes as a little-endian signed `Int`.
-///
-/// Deprecated: prefer bit-pattern matching (`u32le`) directly.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x2A\x00\x00\x00"
-/// guard! bytes is [u32le(u), ..]
-/// inspect(u.reinterpret_as_int(), content="42")
-/// }
-/// ```
-#deprecated
-#doc(hidden)
-pub fn BytesView::to_int_le(self : BytesView) -> Int {
- guard! self is [u32le(u32), ..]
- u32.reinterpret_as_int()
-}
-
-///|
-/// Interpret the first 8 bytes as a big-endian signed `Int64`.
-///
-/// Deprecated: prefer bit-pattern matching (`u64be`) directly.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x00\x00\x00\x00\x00\x00\x00\x2A"
-/// guard! bytes is [u64be(u), ..]
-/// inspect(u.reinterpret_as_int64(), content="42")
-/// }
-/// ```
-#deprecated
-#doc(hidden)
-pub fn BytesView::to_int64_be(self : BytesView) -> Int64 {
- guard! self is [u64be(u64), ..]
- u64.reinterpret_as_int64()
-}
-
-///|
-/// Interpret the first 8 bytes as a little-endian signed `Int64`.
-///
-/// Deprecated: prefer bit-pattern matching (`u64le`) directly.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x2A\x00\x00\x00\x00\x00\x00\x00"
-/// guard! bytes is [u64le(u), ..]
-/// inspect(u.reinterpret_as_int64(), content="42")
-/// }
-/// ```
-#deprecated
-#doc(hidden)
-pub fn BytesView::to_int64_le(self : BytesView) -> Int64 {
- guard! self is [u64le(u64), ..]
- u64.reinterpret_as_int64()
-}
-
-///|
-/// Converts the bytes in a byte view to a double-precision floating-point number
-/// using big-endian byte order. The byte view must contain exactly 8 bytes,
-/// which represent the IEEE 754 double-precision format.
-///
-/// Parameters:
-///
-/// * `byte_view` : The byte view containing exactly 8 bytes to be interpreted as
-/// a double-precision floating-point number in big-endian order.
-///
-/// Returns a double-precision floating-point number reconstructed from the
-/// bytes.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// // Bytes representing 1.0 in IEEE 754 double-precision format (big-endian)
-/// let bytes = b"\x3F\xF0\x00\x00\x00\x00\x00\x00"
-/// guard! bytes is [u64be(bits), ..]
-/// inspect(bits.reinterpret_as_double(), content="1")
-/// }
-/// ```
-#deprecated("Use bits pattern directly")
-#doc(hidden)
-pub fn BytesView::to_double_be(self : BytesView) -> Double {
- guard! self is [u64be(u64), ..]
- u64.reinterpret_as_double()
-}
+fn write_byte_hex(logger : &Logger, byte : Byte) -> Unit {
+ fn hex_digit(value : Int) -> Char {
+ if value < 10 {
+ (value + '0'.to_int()).unsafe_to_char()
+ } else {
+ (value - 10 + 'a'.to_int()).unsafe_to_char()
+ }
+ }
-///|
-/// Converts the bytes in the view to a double-precision floating-point number
-/// using little-endian byte order. Interprets the first 8 bytes as a IEEE 754
-/// double-precision binary floating-point format (binary64) value.
-///
-/// Parameters:
-///
-/// * `bytes` : The byte view to be converted. Must contain at least 8 bytes.
-///
-/// Returns a `Double` value representing the bytes interpreted in little-endian
-/// order.
-///
-/// Example:
-///
-/// ```mbt check
-/// test {
-/// let bytes = b"\x00\x00\x00\x00\x00\x00\xF0\x3F" // represents 1.0 in little-endian
-/// guard! bytes is [u64le(bits), ..]
-/// inspect(bits.reinterpret_as_double(), content="1")
-/// }
-/// ```
-#deprecated("Use bits pattern directly")
-#doc(hidden)
-pub fn BytesView::to_double_le(self : BytesView) -> Double {
- guard! self is [u64le(u64), ..]
- u64.reinterpret_as_double()
+ let value = byte.to_int()
+ logger.write_char(hex_digit(value >> 4))
+ logger.write_char(hex_digit(value & 0x0f))
}
///|
@@ -615,7 +353,7 @@ pub impl Show for BytesView with fn output(self, logger) {
logger.write_char(byte.to_char())
} else {
logger.write_string("\\x")
- logger.write_string(byte.to_hex())
+ write_byte_hex(logger, byte)
}
}
logger.write_string("\"")
@@ -718,12 +456,7 @@ pub impl Compare for BytesView with fn compare(self, other) -> Int {
let other_len = other.length()
let cmp = self_len.compare(other_len)
guard cmp == 0 else { return cmp }
- for i, b1 in self {
- let b2 = other.unsafe_get(i)
- let cmp = b1.compare(b2)
- guard cmp == 0 else { return cmp }
- }
- 0
+ self.lexical_compare(other)
}
///|
@@ -865,7 +598,7 @@ pub impl ToJson for BytesView with fn to_json(self) -> Json {
sb.write_char(byte.to_char())
} else {
sb.write_string("\\x")
- sb.write_string(byte.to_hex())
+ write_byte_hex(sb, byte)
}
}
Json::string(sb.to_string())
diff --git a/builtin/bytesview_compare_bench_test.mbt b/builtin/bytesview_compare_bench_test.mbt
new file mode 100644
index 0000000000..8bc280e853
--- /dev/null
+++ b/builtin/bytesview_compare_bench_test.mbt
@@ -0,0 +1,33 @@
+// 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.
+
+///|
+test "bench BytesView Compare equal n=1000000" (it : @bench.T) {
+ let left = Bytes::makei(1000000, i => i.to_byte())[:]
+ let right = Bytes::makei(1000000, i => i.to_byte())[:]
+ it.bench(fn() { it.keep(left.compare(right)) })
+}
+
+///|
+test "bench BytesView Compare last-diff n=1000000" (it : @bench.T) {
+ let left = Bytes::makei(1000000, i => i.to_byte())[:]
+ let right = Bytes::makei(1000000, i => {
+ if i == 999999 {
+ b'\xff'
+ } else {
+ i.to_byte()
+ }
+ })[:]
+ it.bench(fn() { it.keep(left.compare(right)) })
+}
diff --git a/builtin/char.mbt b/builtin/char.mbt
index 8fbdca49ab..964fd6db44 100644
--- a/builtin/char.mbt
+++ b/builtin/char.mbt
@@ -136,7 +136,7 @@ pub fn Char::is_ascii_uppercase(self : Self) -> Bool {
/// Checks if the value is an ASCII whitespace character:
/// U+0020 SPACE, U+0009 HORIZONTAL TAB, U+000A LINE FEED, U+000B VERTICAL TAB, U+000C FORM FEED, or U+000D CARRIAGE RETURN.
pub fn Char::is_ascii_whitespace(self : Self) -> Bool {
- self is ('\u{20}' | '\u{09}' | '\u{0A}' | '\u{0B}' | '\u{0C}' | '\u{0D}')
+ self is ('\u{09}'..='\u{0D}' | ' ')
}
///|
@@ -164,10 +164,11 @@ pub fn Char::is_digit(self : Self, radix : UInt) -> Bool {
///|
/// Returns true if this char has the White_Space property.
pub fn Char::is_whitespace(self : Self) -> Bool {
+ if self <= '\u{20}' {
+ return self is ('\u{09}'..='\u{0D}' | ' ')
+ }
self
- is ('\u0009'..='\u000D'
- | '\u0020'
- | '\u0085'
+ is ('\u0085'
| '\u00A0'
| '\u1680'
| '\u2000'..='\u200A'
@@ -181,9 +182,14 @@ pub fn Char::is_whitespace(self : Self) -> Bool {
///|
/// Returns true if this char has one of the general categories for numbers.
pub fn Char::is_numeric(self : Self) -> Bool {
+ if self is ('0'..='9') {
+ return true
+ }
+ if self < '\u{B2}' {
+ return false
+ }
self
- is ('\u0030'..='\u0039'
- | '\u00B2'
+ is ('\u00B2'
| '\u00B3'
| '\u00B9'
| '\u00BC'
@@ -342,7 +348,7 @@ pub fn Char::is_numeric(self : Self) -> Bool {
/// - Format characters (Cf)
/// - Line/paragraph separators (Zl, Zp)
/// - Private use (Co)
-/// - Unassigned (Cn)
+/// - Noncharacters (U+FDD0-U+FDEF and the U+nFFFE/U+nFFFF pairs)
/// - Surrogates (Cs)
pub fn Char::is_printable(self : Self) -> Bool {
// Check for control characters (Cc)
diff --git a/builtin/console.mbt b/builtin/console.mbt
index 8d36334e30..745a0ec11f 100644
--- a/builtin/console.mbt
+++ b/builtin/console.mbt
@@ -49,7 +49,7 @@ pub fn[T : Show] println(input : T) -> Unit {
/// ```mbt check
/// test {
/// let x : Int = 42
-/// inspect(x, content="42") // Raises InspectError with detailed failure message
+/// inspect(x, content="42") // A mismatch would raise InspectError with a detailed message
/// }
/// ```
pub(all) suberror InspectError {
@@ -81,8 +81,7 @@ fn base64_encode(data : FixedArray[Byte]) -> String {
let x1 = base64[(b0 & 0x03) << 4]
buf.write_char(x0.to_char())
buf.write_char(x1.to_char())
- buf.write_char('=')
- buf.write_char('=')
+ buf <+ "=="
} else if rem == 2 {
let b0 = data[len - 2].to_int()
let b1 = data[len - 1].to_int()
diff --git a/builtin/deprecated.mbt b/builtin/deprecated.mbt
index 391b496b43..2fe5f554df 100644
--- a/builtin/deprecated.mbt
+++ b/builtin/deprecated.mbt
@@ -65,13 +65,13 @@ pub fn[K, V] Map::new(capacity? : Int = default_init_capacity) -> Map[K, V] {
}
///|
+/// Shifts the bits of a `Byte` value to the left by the given number of bit
/// positions.
///
/// Parameters:
///
-/// - `byte_value` : The `Byte` value whose bits are to be shifted.
-/// - `shift_count` : The number of bit positions to shift the `byte_value` to
-/// the left.
+/// - `self` : The `Byte` value whose bits are to be shifted.
+/// - `count` : The number of bit positions to shift `self` to the left.
///
/// Returns the resulting `Byte` value after the bitwise left shift operation.
///
@@ -82,12 +82,13 @@ pub fn Byte::lsl(self : Byte, count : Int) -> Byte {
}
///|
-/// bits.
+/// Performs a logical (zero-filling) right shift of a `Byte` value by the
+/// given number of bits.
///
/// Parameters:
///
-/// - `value` : The `Byte` value to be shifted.
-/// - `count` : The number of bits to shift the `value` to the right.
+/// - `self` : The `Byte` value to be shifted.
+/// - `count` : The number of bits to shift `self` to the right.
///
/// Returns the result of the logical shift right operation as a `Byte`.
///
@@ -109,3 +110,12 @@ pub fn String::unsafe_char_at(self : String, index : Int) -> Char {
c1.unsafe_to_char()
}
}
+
+///|
+/// Creates a new empty array with an optional initial capacity.
+///
+/// Deprecated: use `Array(capacity=...)` instead.
+#deprecated("Use `Array(capacity=...)` instead")
+pub fn[T] Array::new(capacity? : Int = 0) -> Array[T] {
+ Array(capacity~)
+}
diff --git a/builtin/double.mbt b/builtin/double.mbt
index b6795b3d31..82fe70ebd5 100644
--- a/builtin/double.mbt
+++ b/builtin/double.mbt
@@ -55,8 +55,8 @@ pub fn Double::from_int(i : Int) -> Double {
/// * `value` : The double-precision floating-point number to compute the
/// absolute value of.
///
-/// Returns the absolute value of the input number. For any input `x`, the result
-/// is equivalent to `if x < 0.0 { -x } else { x }`.
+/// Returns the absolute value of the input number by clearing its sign bit, so
+/// the result is never negative. In particular, `(-0.0).abs()` is `0.0`.
///
/// Example:
///
@@ -111,6 +111,7 @@ pub fn Double::max(self : Double, other : Double) -> Double {
/// * `max` : The upper bound of the range.
///
/// Returns `min` if `self < min`, `max` if `self > max`, and `self` otherwise.
+/// Aborts if `min` is greater than `max`.
///
/// Example:
///
@@ -319,7 +320,8 @@ pub impl Show for Double with fn to_string(self) {
///
/// Returns whether the two numbers are considered approximately equal. Returns
/// `true` if the numbers are exactly equal or if they are within either the
-/// relative or absolute tolerance. Returns `false` if either number is infinite.
+/// relative or absolute tolerance. Returns `false` if the numbers are not
+/// exactly equal and either of them is infinite.
///
/// Example:
///
diff --git a/builtin/double_ryu_int_wbtest.mbt b/builtin/double_ryu_int_wbtest.mbt
new file mode 100644
index 0000000000..9c14ca6e1a
--- /dev/null
+++ b/builtin/double_ryu_int_wbtest.mbt
@@ -0,0 +1,109 @@
+// 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.
+
+///|
+/// The integral fast path must be indistinguishable from the full algorithm.
+/// The oracle is `Int::to_string`, which is the definition of the shortest
+/// decimal form for an integer; for values outside the fast path the expected
+/// strings are pinned so a widened guard cannot silently change them.
+test "ryu integral fast path agrees with Int::to_string over a dense band" {
+ for i in -3000..<=3000 {
+ assert_true(ryu_to_string(i.to_double()) == i.to_string())
+ }
+}
+
+///|
+test "ryu integral fast path at the Int range boundary" {
+ // Inside the guard, both signs.
+ inspect(ryu_to_string(2147483646.0), content="2147483646")
+ inspect(ryu_to_string(2147483647.0), content="2147483647")
+ inspect(ryu_to_string(-2147483647.0), content="-2147483647")
+ inspect(ryu_to_string(-2147483648.0), content="-2147483648")
+ // Just outside Int range: now served by the Int64 tier, same rendering.
+ inspect(ryu_to_string(2147483648.0), content="2147483648")
+ inspect(ryu_to_string(2147483649.0), content="2147483649")
+ inspect(ryu_to_string(-2147483649.0), content="-2147483649")
+ inspect(ryu_to_string(-2147483650.0), content="-2147483650")
+}
+
+///|
+test "ryu integral fast path never captures special or fractional values" {
+ // -0.0 is handled by the existing zero check before the fast path.
+ inspect(ryu_to_string(-0.0), content="0")
+ inspect(ryu_to_string(0.0 / 0.0), content="NaN")
+ inspect(ryu_to_string(1.0 / 0.0), content="Infinity")
+ inspect(ryu_to_string(-1.0 / 0.0), content="-Infinity")
+ // Fractional values whose truncation is in range must not take the path.
+ inspect(ryu_to_string(0.5), content="0.5")
+ inspect(ryu_to_string(-0.5), content="-0.5")
+ inspect(ryu_to_string(2147483646.5), content="2147483646.5")
+ inspect(ryu_to_string(-2147483647.5), content="-2147483647.5")
+ // The next representable double below 1.0 and above 1.0: integrality test
+ // must reject both.
+ inspect(ryu_to_string(0.9999999999999999), content="0.9999999999999999")
+ inspect(ryu_to_string(1.0000000000000002), content="1.0000000000000002")
+}
+
+///|
+test "ryu integral fast path across power-of-two neighbourhoods" {
+ // Exact powers of two and neighbours on both sides of the guard, both signs.
+ for p in 0..<=53 {
+ let v = (1L << p).to_double()
+ for d in -2L..<=2L {
+ let x = v + d.to_double()
+ let expected = x.to_int64().to_string()
+ assert_true(ryu_to_string(x) == expected)
+ let nx = -v + d.to_double()
+ let nexpected = if nx == 0.0 { "0" } else { nx.to_int64().to_string() }
+ assert_true(ryu_to_string(nx) == nexpected)
+ }
+ }
+}
+
+///|
+test "ryu integral fast path at the 2^53 boundary" {
+ // 2^53 is the inclusive bound of MAX_EXACTLY_REPRESENTABLE_INT.
+ inspect(ryu_to_string(9007199254740992.0), content="9007199254740992")
+ inspect(ryu_to_string(-9007199254740992.0), content="-9007199254740992")
+ inspect(ryu_to_string(9007199254740991.0), content="9007199254740991")
+ inspect(ryu_to_string(-9007199254740991.0), content="-9007199254740991")
+ // 2^53 + 2 is the first representable integer past the bound: it must fall
+ // through to the full algorithm and render identically to before.
+ inspect(ryu_to_string(9007199254740994.0), content="9007199254740994")
+ inspect(ryu_to_string(-9007199254740994.0), content="-9007199254740994")
+ // Values between Int and Int64 tiers: a millisecond timestamp.
+ inspect(ryu_to_string(1700000000000.0), content="1700000000000")
+ inspect(ryu_to_string(-1700000000000.0), content="-1700000000000")
+}
+
+///|
+test "ryu integral fast path rejects fractional values in the Int64 tier" {
+ // These sit between the Int tier and the 2^53 bound; truncation is exact
+ // but the values are not integral, so they must use the full algorithm.
+ inspect(ryu_to_string(1700000000000.5), content="1700000000000.5")
+ inspect(ryu_to_string(-1700000000000.5), content="-1700000000000.5")
+ inspect(ryu_to_string(4503599627370495.5), content="4503599627370495.5")
+}
+
+///|
+test "ryu bound is load-bearing above 2^53" {
+ // 2^62 is exactly representable and round-trips through Int64, but its
+ // SHORTEST decimal form is not its exact integer digits (4611686018427387904):
+ // doubles are spaced 512 apart here, so a shorter decimal round-trips.
+ // Widening MAX_EXACTLY_REPRESENTABLE_INT past 2^53 would break this.
+ inspect(ryu_to_string(4611686018427387904.0), content="4611686018427388000")
+ inspect(ryu_to_string(-4611686018427387904.0), content="-4611686018427388000")
+ // 2^60: spacing 128; its shortest form is also shortened, not exact.
+ inspect(ryu_to_string(1152921504606846976.0), content="1152921504606847000")
+}
diff --git a/builtin/double_ryu_nonjs.mbt b/builtin/double_ryu_nonjs.mbt
index 22b0ce0446..0139ee470b 100644
--- a/builtin/double_ryu_nonjs.mbt
+++ b/builtin/double_ryu_nonjs.mbt
@@ -522,7 +522,7 @@ fn d2d(ieeeMantissa : UInt64, ieeeExponent : UInt) -> FloatingDecimal64 {
output = vr + (vr == vm || roundUp).to_uint64()
}
let exp : Int = e10 + removed
- let fd : FloatingDecimal64 = { mantissa: output, exponent: exp }
+ let fd : FloatingDecimal64 = { mantissa: output, exponent: exp, }
fd
}
@@ -651,15 +651,46 @@ fn d2d_small_int(
if fraction != 0UL {
return None
}
- Some({ mantissa: m2 >> -e2, exponent: 0 })
+ Some({ mantissa: m2 >> -e2, exponent: 0, })
}
+///|
+/// The largest double below which EVERY integer is exactly representable as
+/// binary64: 2^53. Individual larger integers (2^62, say) still round-trip
+/// through Int64, but their shortest decimal form is no longer their exact
+/// digits, so they must not take the integer fast path. (JavaScript's
+/// MAX_SAFE_INTEGER is 2^53 - 1; the bound here includes 2^53 itself, which
+/// is exactly representable and renders identically either way.)
+const MAX_EXACTLY_REPRESENTABLE_INT : Double = 9_007_199_254_740_992
+
///|
/// TODO: ryu_to_logger[T:Logger](Double/Float, T) -> Unit
fn ryu_to_string(val : Double) -> String {
if val == 0.0 {
return "0"
}
+ // Integral values up to 2^53 have the same shortest decimal form as the
+ // corresponding integer (every integer in this range is exactly
+ // representable, so no shorter decimal can round-trip to it), and integer
+ // formatting is far cheaper than the full shortest-representation search
+ // below. NaN and the infinities fail the range comparisons; -0.0 is handled
+ // by the zero check above; fractional values fail the round-trip check.
+ if val >= -MAX_EXACTLY_REPRESENTABLE_INT &&
+ val <= MAX_EXACTLY_REPRESENTABLE_INT {
+ // Two tiers: Int formatting beats Int64 formatting for the values that
+ // dominate real workloads, so take it when the value fits.
+ if val >= -2147483648.0 && val <= 2147483647.0 {
+ let i = val.to_int()
+ if i.to_double() == val {
+ return i.to_string()
+ }
+ } else {
+ let i = val.to_int64()
+ if i.to_double() == val {
+ return i.to_string()
+ }
+ }
+ }
// Step 1: Decode the floating-point number, and unify normalized and subnormal cases.
let bits : UInt64 = val.reinterpret_as_uint64()
@@ -683,7 +714,7 @@ fn ryu_to_string(val : Double) -> String {
if r != 0 {
break x
}
- continue { mantissa: q, exponent: x.exponent + 1 }
+ continue { mantissa: q, exponent: x.exponent + 1, }
}
None => d2d(ieeeMantissa, ieeeExponent.reinterpret_as_uint())
}
diff --git a/builtin/double_to_int64_js_wasm.mbt b/builtin/double_to_int64_js_wasm.mbt
index 516fb8f72b..c044369e61 100644
--- a/builtin/double_to_int64_js_wasm.mbt
+++ b/builtin/double_to_int64_js_wasm.mbt
@@ -53,8 +53,8 @@ pub fn Double::to_int64(self : Double) -> Int64 = "%f64_to_i64_saturate"
/// Returns an unsigned 64-bit integer value according to the following rules:
///
/// * Returns 0 if the input is NaN
-/// * Returns `UInt64::max_value` (18446744073709551615UL) if the input is
-/// greater than or equal to `UInt64::max_value`
+/// * Returns `@uint64.MAX_VALUE` (18446744073709551615UL) if the input is
+/// greater than or equal to `@uint64.MAX_VALUE`
/// * Returns 0UL if the input is less than or equal to 0
/// * Otherwise returns the integer part of the input by truncating towards zero
///
diff --git a/builtin/extends.mbt b/builtin/extends.mbt
index dc0a1ab4cc..70e511f100 100644
--- a/builtin/extends.mbt
+++ b/builtin/extends.mbt
@@ -1083,11 +1083,6 @@ pub extend Int64 with Show::{output}
#doc(hidden)
pub extend Iter with Show::{to_string, output}
-///|
-#deprecated("Use `@debug.Debug` instead of `Show` for collections", skip_current_package=true)
-#doc(hidden)
-pub extend Iter2 with Show::{to_string, output}
-
///|
#deprecated("Use `Default::default` instead", skip_current_package=true)
#doc(hidden)
diff --git a/builtin/failure.mbt b/builtin/failure.mbt
index 12ebd07d17..94710abbfa 100644
--- a/builtin/failure.mbt
+++ b/builtin/failure.mbt
@@ -34,6 +34,8 @@
/// @json.json_inspect(err, content=["Failure", "Test assertion failed"])
/// }
/// ```
+// TODO(future): `derive(Show)` is deprecated syntax — switch to `derive(Debug)`
+// or a manual `Show` implementation, then remove this annotation.
#warnings("-deprecated_syntax")
pub(all) suberror Failure {
Failure(String)
diff --git a/builtin/feature_test.mbt b/builtin/feature_test.mbt
index fb38a33b7c..e11d57b0df 100644
--- a/builtin/feature_test.mbt
+++ b/builtin/feature_test.mbt
@@ -29,12 +29,12 @@ trait Range {
///|
impl Range for Int with fn range(lo, hi) {
- { lo, hi }
+ { lo, hi, }
}
///|
impl Range for String with fn range(lo, hi) {
- { lo, hi }
+ { lo, hi, }
}
///|
diff --git a/builtin/fixedarray.mbt b/builtin/fixedarray.mbt
index 23108f7a8d..2403f31a37 100644
--- a/builtin/fixedarray.mbt
+++ b/builtin/fixedarray.mbt
@@ -33,6 +33,7 @@
/// debug_inspect(arr.get(3), content="None")
/// }
/// ```
+#intrinsic("%fixedarray.get_opt")
pub fn[T] FixedArray::get(self : FixedArray[T], idx : Int) -> T? {
let len = self.length()
guard idx >= 0 && idx < len else { None }
@@ -774,9 +775,8 @@ pub fn[A, B] FixedArray::rev_foldi(
init~ : B,
f : (Int, B, A) -> B raise?,
) -> B raise? {
- let len = self.length()
- for i in len>..0; acc = init {
- continue f(len - i - 1, acc, self.unsafe_get(i))
+ for i in self.length()>..0; index = 0, acc = init {
+ continue index + 1, f(index, acc, self.unsafe_get(i))
} nobreak {
acc
}
@@ -797,6 +797,16 @@ test "rev_foldi" {
inspect(sum, content="25")
}
+///|
+test "rev_foldi preserves reversed index element pairs" {
+ let trace = ([10, 20, 30] : FixedArray[_]).rev_foldi(init=0, (
+ index,
+ acc,
+ elem,
+ ) => acc * 1000 + index * 100 + elem)
+ inspect(trace, content="30120210")
+}
+
///|
/// Reverses the array in place by swapping elements from both ends until
/// reaching the middle.
@@ -1387,8 +1397,7 @@ test "iter" {
let exb = StringBuilder()
let mut i = 0
iter.each(x => {
- exb.write_string(x.to_string())
- exb.write_char('\n')
+ exb <+ "\{x}\n"
i += 1
})
assert_true(i == arr.length())
@@ -1555,7 +1564,7 @@ test "FixedArray::join" {
///|
/// Return an iterator over elements of the fixed array.
///
-/// Deprecated alias; prefer `iterator`.
+/// `iterator` is a deprecated alias for this function.
///
/// Example:
///
@@ -1575,7 +1584,7 @@ pub fn[X] FixedArray::iter(self : FixedArray[X]) -> Iter[X] {
///|
/// Return an index-value iterator over the fixed array.
///
-/// Deprecated alias; prefer `iterator2`.
+/// `iterator2` is a deprecated alias for this function.
///
/// Example:
///
diff --git a/builtin/fixedarray_test.mbt b/builtin/fixedarray_test.mbt
index 044211e0e3..094c0fced9 100644
--- a/builtin/fixedarray_test.mbt
+++ b/builtin/fixedarray_test.mbt
@@ -175,12 +175,12 @@ struct TestStruct2 {
///|
test "fixedarray_binary_search_by_test" {
let arr : FixedArray[TestStruct2] = [
- { num2: 10 },
- { num2: 22 },
- { num2: 35 },
- { num2: 48 },
+ { num2: 10, },
+ { num2: 22, },
+ { num2: 35, },
+ { num2: 48, },
]
- let mut target : TestStruct2 = { num2: 22 }
+ let mut target : TestStruct2 = { num2: 22, }
fn cmp(val : TestStruct2) {
if val.num2 < target.num2 {
-1
@@ -192,11 +192,11 @@ test "fixedarray_binary_search_by_test" {
}
@test.assert_eq(arr.binary_search_by(cmp), Ok(1))
- target = { num2: 48 }
+ target = { num2: 48, }
@test.assert_eq(arr.binary_search_by(cmp), Ok(3))
- target = { num2: -8 }
+ target = { num2: -8, }
@test.assert_eq(arr.binary_search_by(cmp), Err(0))
- target = { num2: 49 }
+ target = { num2: 49, }
@test.assert_eq(arr.binary_search_by(cmp), Err(4))
}
@@ -423,21 +423,21 @@ test "FixedArray::from_array with array of different type" {
///|
test "FixedArray::from_iter with multiple elements iterator" {
- let iter = [1, 2, 3, 4, 5].iter()
+ let iter = [|1, 2, 3, 4, 5|]
let result = FixedArray::from_iter(iter)
@test.assert_eq(result, [1, 2, 3, 4, 5])
}
///|
test "FixedArray::from_iter with single element iterator" {
- let iter = [1].iter()
+ let iter = [|1|]
let result = FixedArray::from_iter(iter)
@test.assert_eq(result, [1])
}
///|
test "FixedArray::from_iter with empty iterator" {
- let iter : Iter[Int] = Iter::empty()
+ let iter : Iter[Int] = [||]
let result = FixedArray::from_iter(iter)
@test.assert_eq(result, [])
}
@@ -471,13 +471,13 @@ test "FixedArray::arbitrary" {
#| ,
#| ,
#| ,
- #| ,
- #| ,
+ #| ,
+ #| ,
#| ,
#| ,
- #| ,
- #| ,
- #| ,
+ #| ,
+ #| ,
+ #| ,
#| ]>
),
)
@@ -486,11 +486,35 @@ test "FixedArray::arbitrary" {
content=(
#|,
- #| ,
- #| ,
- #| ,
- #| ,
+ #| ,
+ #| ,
+ #| ,
+ #| ,
+ #| ,
#| ]>
),
)
diff --git a/builtin/hasher.mbt b/builtin/hasher.mbt
index eb320929c6..8702fdd4d7 100644
--- a/builtin/hasher.mbt
+++ b/builtin/hasher.mbt
@@ -57,7 +57,9 @@ struct Hasher {
/// Parameters:
///
/// * `seed` : An integer value used to initialize the hasher's internal state.
-/// Defaults to 0.
+/// When omitted, a randomly chosen process-wide seed is used on native, LLVM,
+/// and JavaScript targets. Wasm targets default to 0. Pass `0` explicitly when
+/// deterministic output is required.
///
/// Returns a new `Hasher` instance initialized with the given seed value.
///
@@ -77,11 +79,23 @@ struct Hasher {
/// `Hasher::new` remains available as a deprecated alias.
#alias(new, deprecated="Use `Hasher()` instead")
pub fn Hasher::Hasher(seed? : Int = seed) -> Hasher {
- { acc: seed.reinterpret_as_uint() + GPRIME5 }
+ { acc: seed.reinterpret_as_uint() + GPRIME5, }
}
///|
-#cfg(not(target="js"))
+// `builtin` cannot depend on `env`, so bind the runtime entropy source here.
+#cfg(any(target="native", target="llvm"))
+let seed : Int = {
+ let bytes : FixedArray[Byte] = FixedArray::make(4, b'\x00')
+ if moonbit_rt_get_random_for_hash_seed(bytes) == 0 {
+ fixedarray_read_uint32_le(bytes, 0).reinterpret_as_int()
+ } else {
+ 0
+ }
+}
+
+///|
+#cfg(any(target="wasm", target="wasm-gc"))
let seed : Int = 0
///|
@@ -101,6 +115,13 @@ extern "js" fn random_seed() -> Int =
#| }
#|}
+///|
+#cfg(any(target="native", target="llvm"))
+#borrow(bytes)
+extern "c" fn moonbit_rt_get_random_for_hash_seed(
+ bytes : FixedArray[Byte],
+) -> Int = "moonbit_rt_get_random"
+
///|
/// Combines a hashable value with the current state of the hasher. This is
/// typically used to incrementally build a hash value from multiple components.
@@ -358,15 +379,22 @@ pub fn Hasher::combine_byte(self : Hasher, value : Byte) -> Unit {
/// }
/// ```
pub fn Hasher::combine_bytes(self : Hasher, value : Bytes) -> Unit {
- let (cur, remain) = for cur = 0, remain = value.length(); remain >= 4; {
- self.consume4(endian32(value, cur))
- continue cur + 4, remain - 4
- } nobreak {
- (cur, remain)
- }
- for cur = cur, remain = remain; remain >= 1; {
- self.consume1(value[cur])
- continue cur + 1, remain - 1
+ for data = value.view() {
+ if data is [u32le(x), .. rest] {
+ self.consume4(x)
+ continue rest
+ } else {
+ if data is [b0, .. data] {
+ self.consume1(b0)
+ if data is [b1, .. data] {
+ self.consume1(b1)
+ if data is [b2, ..] {
+ self.consume1(b2)
+ }
+ }
+ }
+ break
+ }
}
}
@@ -476,16 +504,6 @@ fn rotl(x : UInt, r : Int) -> UInt {
(x << r) | (x >> (32 - r))
}
-///|
-fn endian32(input : Bytes, cur : Int) -> UInt {
- input[cur + 0].to_uint() |
- (
- (input[cur + 1].to_uint() << 8) |
- (input[cur + 2].to_uint() << 16) |
- (input[cur + 3].to_uint() << 24)
- )
-}
-
///|
/// Implements the `Hash` trait for `String` type, providing a method to combine
/// a string's hash value with a hasher's state.
@@ -735,20 +753,22 @@ pub impl Hash for BytesView with fn hash_combine(
self : BytesView,
hasher : Hasher,
) {
- let data = self.bytes()
- let (start, rest) = for start = self.start(), rest = self.len(); rest >= 4; {
- let result = for i in 0..<=3; result = (0 : UInt) {
- continue result | (data.unsafe_get(i + start).to_uint() << (8 * i))
- } nobreak {
- result
+ for data = self {
+ if data is [u32le(x), .. rest] {
+ hasher.combine_uint(x)
+ continue rest
+ } else {
+ // only 0-3 bytes left
+ if data is [b0, .. data] {
+ hasher.combine_byte(b0)
+ if data is [b1, .. data] {
+ hasher.combine_byte(b1)
+ if data is [b2, ..] {
+ hasher.combine_byte(b2)
+ }
+ }
+ }
+ break
}
- hasher.combine_uint(result)
- continue start + 4, rest - 4
- } nobreak {
- (start, rest)
- }
- for start = start, rest = rest; rest >= 1; {
- hasher.combine_byte(data.unsafe_get(start))
- continue start + 1, rest - 1
}
}
diff --git a/builtin/int.mbt b/builtin/int.mbt
index 6262a76b5d..74de583e71 100644
--- a/builtin/int.mbt
+++ b/builtin/int.mbt
@@ -94,7 +94,8 @@ pub fn Int::max(self : Int, other : Int) -> Int {
}
///|
-/// Clamps the value `self` between `min` and `max`.
+/// Clamps the value `self` between `min` and `max`. Aborts if `min` is greater
+/// than `max`.
///
/// Example:
/// ```mbt check
@@ -175,7 +176,9 @@ pub fn Int::is_surrogate(self : Int) -> Bool {
///
/// * `self` : The integer whose absolute value is to be computed.
///
-/// Returns the absolute value of the integer.
+/// Returns the absolute value of the integer. When the input is
+/// `@int.min_value` (-2147483648), returns `@int.min_value` itself, since its
+/// absolute value is not representable as an `Int`.
///
/// Example:
///
diff --git a/builtin/int64.mbt b/builtin/int64.mbt
index 6b6841a820..41951d65d6 100644
--- a/builtin/int64.mbt
+++ b/builtin/int64.mbt
@@ -52,7 +52,9 @@ pub fn Int64::from_int(i : Int) -> Int64 {
///
/// * `self` : The 64-bit integer whose absolute value is to be computed.
///
-/// Returns the absolute value of the input integer.
+/// Returns the absolute value of the input integer. When the input is
+/// `@int64.MIN_VALUE` (-9223372036854775808), returns `@int64.MIN_VALUE`
+/// itself, since its absolute value is not representable as an `Int64`.
///
/// Example:
///
@@ -72,7 +74,24 @@ pub fn Int64::abs(self : Int64) -> Int64 {
}
///|
-/// Returns the minimum of two 64-bit signed integers.
+/// Returns the smaller of two 64-bit signed integers.
+///
+/// Parameters:
+///
+/// * `self` : The first integer to compare.
+/// * `other` : The second integer to compare.
+///
+/// Returns `self` if it is not greater than `other`, `other` otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// inspect(1L.min(2L), content="1")
+/// inspect(2L.min(1L), content="1")
+/// inspect((-1L).min(0L), content="-1")
+/// }
+/// ```
pub fn Int64::min(self : Int64, other : Int64) -> Int64 {
if self < other {
self
@@ -82,7 +101,24 @@ pub fn Int64::min(self : Int64, other : Int64) -> Int64 {
}
///|
-/// Returns the maximum of two 64-bit signed integers.
+/// Returns the larger of two 64-bit signed integers.
+///
+/// Parameters:
+///
+/// * `self` : The first integer to compare.
+/// * `other` : The second integer to compare.
+///
+/// Returns `self` if it is not less than `other`, `other` otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// inspect(1L.max(2L), content="2")
+/// inspect(2L.max(1L), content="2")
+/// inspect((-1L).max(0L), content="0")
+/// }
+/// ```
pub fn Int64::max(self : Int64, other : Int64) -> Int64 {
if self > other {
self
@@ -92,7 +128,26 @@ pub fn Int64::max(self : Int64, other : Int64) -> Int64 {
}
///|
-/// Clamps the value `self` between `min` and `max`.
+/// Clamps a 64-bit signed integer into the inclusive range [`min`, `max`].
+///
+/// Parameters:
+///
+/// * `self` : The value to clamp.
+/// * `min` : The lower bound of the range.
+/// * `max` : The upper bound of the range.
+///
+/// Returns `min` if `self` is less than `min`, `max` if `self` is greater
+/// than `max`, and `self` otherwise. Aborts if `min` is greater than `max`.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// inspect(5L.clamp(min=0L, max=10L), content="5")
+/// inspect((-5L).clamp(min=0L, max=10L), content="0")
+/// inspect(15L.clamp(min=0L, max=10L), content="10")
+/// }
+/// ```
pub fn Int64::clamp(self : Int64, min~ : Int64, max~ : Int64) -> Int64 {
guard! min <= max
if self < min {
@@ -601,7 +656,7 @@ pub fn Int64::clz(self : Int64) -> Int = "%i64_clz"
///
/// ```mbt check
/// test {
-/// let x = 0x7000_0001_1F00_100FL // 0111000000000000000000000001000111110000000100001111
+/// let x = 0x7000_0001_1F00_100FL // Binary: 0111 0000 ... 0001 1111 0000 0000 0001 0000 0000 1111
/// inspect(x.popcnt(), content="14")
/// }
/// ```
@@ -801,7 +856,7 @@ pub fn Int64::reinterpret_as_double(self : Int64) -> Double = "%i64_to_f64_reint
///
/// ```mbt check
/// test {
-/// // 0x4045000000000000 represents 42.0 in IEEE 754 double format
+/// // 0x4059000000000000 represents 100.0 in IEEE 754 double format
/// let n = 4636737291354636288UL
/// inspect(n.reinterpret_as_double(), content="100")
/// }
diff --git a/builtin/intrinsics.mbt b/builtin/intrinsics.mbt
index 770978f6ba..bed44a6563 100644
--- a/builtin/intrinsics.mbt
+++ b/builtin/intrinsics.mbt
@@ -29,7 +29,7 @@
/// let x = 42
/// ignore(x) // Explicitly ignore the value
/// let mut sum = 0
-/// ignore([1, 2, 3].iter().each(x => sum += x)) // Ignore the Unit return value of each()
+/// ignore([|1, 2, 3|].each(x => sum += x)) // Ignore the Unit return value of each()
/// inspect(sum, content="6")
/// }
/// ```
@@ -199,9 +199,9 @@ pub impl Default for Bool with fn default() = "%bool_default"
/// * `self` : The integer value to negate.
///
/// Returns the negation of the input value. For all inputs except
-/// `Int::min_value()`, returns the value with opposite sign. When the input is
-/// `Int::min_value()`, returns `Int::min_value()` due to two's complement
-/// representation.
+/// `@int.min_value` (-2147483648), returns the value with opposite sign. When
+/// the input is `@int.min_value`, returns `@int.min_value` due to two's
+/// complement representation.
///
/// Example:
///
@@ -209,7 +209,7 @@ pub impl Default for Bool with fn default() = "%bool_default"
/// test {
/// inspect(-42, content="-42")
/// inspect(42, content="42")
-/// inspect(2147483647, content="2147483647") // negating near min value
+/// inspect(2147483647, content="2147483647") // Int maximum value
/// }
/// ```
pub impl Neg for Int with fn neg(self) = "%i32_neg"
@@ -647,7 +647,29 @@ pub fn Int::shr(self : Int, other : Int) -> Int = "%i32_shr"
pub fn Int::ctz(self : Int) -> Int = "%i32_ctz"
///|
-/// Count leading zero bits in a 32-bit integer.
+/// Counts the number of consecutive zero bits at the most significant end of
+/// the integer's binary representation.
+///
+/// Parameters:
+///
+/// * `self` : The integer value whose leading zeros are to be counted.
+///
+/// Returns the number of leading zero bits (0 to 32). For example, returns 0 if
+/// the value is negative (the sign bit is 1), returns 32 if the value is 0
+/// (all bits are zeros).
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// let x = 0
+/// inspect(x.clz(), content="32") // All bits are zero
+/// let y = -1
+/// inspect(y.clz(), content="0") // The sign bit is set
+/// let z = 16
+/// inspect(z.clz(), content="27") // Binary: ...00010000
+/// }
+/// ```
pub fn Int::clz(self : Int) -> Int = "%i32_clz"
///|
@@ -753,7 +775,26 @@ pub impl Compare for Int with fn op_gt(x, y) = "%i32.gt"
pub impl Compare for Int with fn op_ge(x, y) = "%i32.ge"
///|
-/// Return `true` if integer is strictly positive.
+/// Tests whether an integer is strictly positive.
+///
+/// Parameters:
+///
+/// * `self` : The integer to test.
+///
+/// Returns `true` if the integer is strictly positive, `false` otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// let neg = -42
+/// let zero = 0
+/// let pos = 42
+/// inspect(neg.is_pos(), content="false")
+/// inspect(zero.is_pos(), content="false")
+/// inspect(pos.is_pos(), content="true")
+/// }
+/// ```
pub fn Int::is_pos(self : Int) -> Bool = "%i32_is_pos"
///|
@@ -780,11 +821,51 @@ pub fn Int::is_pos(self : Int) -> Bool = "%i32_is_pos"
pub fn Int::is_neg(self : Int) -> Bool = "%i32_is_neg"
///|
-/// Return `true` if integer is non-positive (`<= 0`).
+/// Tests whether an integer is non-positive (less than or equal to zero).
+///
+/// Parameters:
+///
+/// * `self` : The integer to test.
+///
+/// Returns `true` if the integer is less than or equal to zero, `false`
+/// otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// let neg = -42
+/// let zero = 0
+/// let pos = 42
+/// inspect(neg.is_non_pos(), content="true")
+/// inspect(zero.is_non_pos(), content="true")
+/// inspect(pos.is_non_pos(), content="false")
+/// }
+/// ```
pub fn Int::is_non_pos(self : Int) -> Bool = "%i32_is_non_pos"
///|
-/// Return `true` if integer is non-negative (`>= 0`).
+/// Tests whether an integer is non-negative (greater than or equal to zero).
+///
+/// Parameters:
+///
+/// * `self` : The integer to test.
+///
+/// Returns `true` if the integer is greater than or equal to zero, `false`
+/// otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// let neg = -42
+/// let zero = 0
+/// let pos = 42
+/// inspect(neg.is_non_neg(), content="false")
+/// inspect(zero.is_non_neg(), content="true")
+/// inspect(pos.is_non_neg(), content="true")
+/// }
+/// ```
pub fn Int::is_non_neg(self : Int) -> Bool = "%i32_is_non_neg"
///|
@@ -1066,9 +1147,8 @@ pub fn Double::sqrt(self : Double) -> Double = "%f64_sqrt"
///|
/// Compares two double-precision floating-point numbers for equality following
-/// IEEE 754 rules. Returns `true` if both numbers are equal, including when both
-/// are `NaN`. Note that this differs from the standard IEEE 754 behavior where
-/// `NaN` is not equal to any value, including itself.
+/// IEEE 754 rules. Returns `true` if both numbers are equal. `NaN` is not equal
+/// to any value, including itself, so `nan == nan` evaluates to `false`.
///
/// Parameters:
///
@@ -1449,7 +1529,8 @@ pub fn Bytes::length(self : Bytes) -> Int = "%bytes_length"
///
/// Parameters:
///
-/// * `length` : The length of the byte sequence to create. Must be non-negative.
+/// * `length` : The length of the byte sequence to create. A negative value
+/// produces an empty byte sequence.
/// * `initial_value` : The byte value used to initialize each position in the
/// sequence.
///
@@ -2622,7 +2703,8 @@ pub fn Int::to_int16(self : Int) -> Int16 = "%i32_to_i16"
///|
/// Converts a byte value to a 16-bit signed integer. The byte value is
-/// sign-extended to 16 bits during the conversion.
+/// zero-extended to 16 bits during the conversion, so the result is always in
+/// the range [0, 255].
///
/// Parameters:
///
diff --git a/builtin/iter_test.mbt b/builtin/iter_test.mbt
index ea81c9be84..c5e4fa5032 100644
--- a/builtin/iter_test.mbt
+++ b/builtin/iter_test.mbt
@@ -14,7 +14,9 @@
///|
test "empty" {
- let iter = Iter::empty()
+ // the `[||]` literal is preferred at call sites, but `Iter::empty` is still
+ // public API and needs its own coverage
+ let iter : Iter[Char] = Iter::empty()
let exb = StringBuilder(size_hint=0)
iter.each(x => exb.write_char(x))
inspect(exb)
@@ -22,6 +24,7 @@ test "empty" {
///|
test "singleton" {
+ // likewise `Iter::singleton`, the constructor behind `[|x|]`
let iter = Iter::singleton('1')
let exb = StringBuilder(size_hint=0)
iter.each(x => exb.write_char(x))
@@ -52,7 +55,7 @@ test "count_if" {
@test.assert_eq(iter.count_if(x => x % 2 == 0), 3)
let iter = test_from_array([1, 2, 3])
@test.assert_eq(iter.count_if(x => x > 99), 0)
- let iter : Iter[Int] = Iter::empty()
+ let iter : Iter[Int] = [||]
@test.assert_eq(iter.count_if(_ => true), 0)
}
@@ -113,10 +116,7 @@ test "take_while" {
test "take_while2" {
let iter = test_from_array(['1', '2', '3', '4', '5'])
let exb = StringBuilder(size_hint=0)
- iter
- .take_while(x => x != '4')
- .concat(Iter::singleton('6'))
- .each(x => exb.write_char(x))
+ iter.take_while(x => x != '4').concat([|'6'|]).each(x => exb.write_char(x))
inspect(exb, content="1236")
}
@@ -125,7 +125,7 @@ test "take_while3" {
let iter = test_from_array([1, 2, 3])
let res = iter
.take_while(x => x != 4)
- .concat(Iter::singleton(4))
+ .concat([|4|])
.find_first(x => x % 2 == 0)
debug_inspect(res, content="Some(2)")
}
@@ -150,7 +150,7 @@ test "map_while1" {
let exb = StringBuilder(size_hint=0)
iter
.map_while(x => if x != 4 { Some(x) } else { None })
- .each(x => exb.write_string("\{x}\n"))
+ .each(x => exb <+ "\{x}\n")
inspect(
exb,
content=(
@@ -168,8 +168,8 @@ test "map_while2" {
let exb = StringBuilder(size_hint=0)
iter
.map_while(x => if x != 4 { Some(x) } else { None })
- .concat(Iter::singleton(6))
- .each(x => exb.write_string("\{x}\n"))
+ .concat([|6|])
+ .each(x => exb <+ "\{x}\n")
inspect(
exb,
content=(
@@ -187,7 +187,7 @@ test "map_while3" {
let iter = test_from_array([1, 2, 3])
let res = iter
.map_while(x => if x != 4 { Some(x) } else { None })
- .concat(Iter::singleton(4))
+ .concat([|4|])
.find_first(x => x % 2 == 0)
debug_inspect(res, content="Some(2)")
}
@@ -240,7 +240,7 @@ test "drop_while2" {
let iter = test_from_array([1, 2, 3, 4, 5])
let res = iter
.drop_while(x => x <= 3)
- .concat(Iter::singleton(6))
+ .concat([|6|])
.find_first(x => x % 3 == 0)
debug_inspect(res, content="Some(6)")
}
@@ -251,7 +251,7 @@ test "drop_while3" {
let exb = StringBuilder(size_hint=0)
let res = iter
.drop_while(x => x < 3)
- .concat(Iter::singleton(6))
+ .concat([|6|])
.find_first(x => {
exb.write_char('x')
x % 3 == 0
@@ -266,7 +266,7 @@ test "drop_while4" {
let iter = test_from_array([1, 2, 3, 4, 5])
let res = iter
.drop_while(x => x <= 3)
- .concat(Iter::singleton(6))
+ .concat([|6|])
.drop(3)
.find_first(x => x % 3 == 0)
debug_inspect(res, content="None")
@@ -292,57 +292,47 @@ test "map" {
///|
test "size_hint" {
- let iter = [1, 2, 3].iter()
+ // an `Iter` literal and `Array::iter` report the same hint
+ debug_inspect([1, 2, 3].iter().size_hint(), content="Some(3)")
+ let iter = [|1, 2, 3|]
debug_inspect(iter.size_hint(), content="Some(3)")
debug_inspect(iter.next(), content="Some(1)")
debug_inspect(iter.size_hint(), content="Some(2)")
debug_inspect(iter.collect(), content="[2, 3]")
debug_inspect(iter.size_hint(), content="Some(0)")
- debug_inspect([1, 2, 3].iter().map(x => x + 1).size_hint(), content="Some(3)")
+ debug_inspect([|1, 2, 3|].map(x => x + 1).size_hint(), content="Some(3)")
debug_inspect(
- [1, 2, 3].iter().mapi((i, x) => i + x).size_hint(),
+ [|1, 2, 3|].mapi((i, x) => i + x).size_hint(),
content="Some(3)",
)
- debug_inspect([1, 2, 3].iter().tap(_ => ()).size_hint(), content="Some(3)")
- debug_inspect([1, 2, 3].iter().take(2).size_hint(), content="Some(2)")
- debug_inspect([1, 2, 3].iter().take(5).size_hint(), content="Some(3)")
- debug_inspect([1, 2, 3].iter().take(0).size_hint(), content="Some(0)")
- debug_inspect([1, 2, 3].iter().drop(0).size_hint(), content="Some(3)")
- debug_inspect([1, 2, 3].iter().drop(1).size_hint(), content="Some(2)")
- debug_inspect([1, 2, 3].iter().drop(5).size_hint(), content="Some(0)")
- debug_inspect(
- [1, 2, 3].iter().concat([4, 5].iter()).size_hint(),
- content="Some(5)",
- )
- debug_inspect(
- [1, 2].iter().zip([3, 4, 5].iter()).size_hint(),
- content="Some(2)",
- )
- debug_inspect(
- [1, 2, 3].iter().zip([4, 5].iter()).size_hint(),
- content="Some(2)",
- )
- let empty_left : Iter[Int] = Iter::empty()
+ debug_inspect([|1, 2, 3|].tap(_ => ()).size_hint(), content="Some(3)")
+ debug_inspect([|1, 2, 3|].take(2).size_hint(), content="Some(2)")
+ debug_inspect([|1, 2, 3|].take(5).size_hint(), content="Some(3)")
+ debug_inspect([|1, 2, 3|].take(0).size_hint(), content="Some(0)")
+ debug_inspect([|1, 2, 3|].drop(0).size_hint(), content="Some(3)")
+ debug_inspect([|1, 2, 3|].drop(1).size_hint(), content="Some(2)")
+ debug_inspect([|1, 2, 3|].drop(5).size_hint(), content="Some(0)")
+ debug_inspect([|1, 2, 3|].concat([|4, 5|]).size_hint(), content="Some(5)")
+ debug_inspect([|1, 2|].zip([|3, 4, 5|]).size_hint(), content="Some(2)")
+ debug_inspect([|1, 2, 3|].zip([|4, 5|]).size_hint(), content="Some(2)")
+ let empty_left : Iter[Int] = [||]
debug_inspect(
empty_left.zip(Iter::new(() => Some(1))).size_hint(),
content="Some(0)",
)
- let empty_right : Iter[Int] = Iter::empty()
+ let empty_right : Iter[Int] = [||]
debug_inspect(
Iter::new(() => Some(1)).zip(empty_right).size_hint(),
content="Some(0)",
)
- debug_inspect(
- Iter::new(() => Some(1)).zip([1].iter()).size_hint(),
- content="None",
- )
- debug_inspect([1, 2, 3].iter().intersperse(0).size_hint(), content="Some(5)")
+ debug_inspect(Iter::new(() => Some(1)).zip([|1|]).size_hint(), content="None")
+ debug_inspect([|1, 2, 3|].intersperse(0).size_hint(), content="Some(5)")
debug_inspect(
Iter::new(() => Some(1)).intersperse(0).size_hint(),
content="None",
)
- debug_inspect([1, 2, 3].iter()[1:3].size_hint(), content="Some(2)")
- debug_inspect([1, 2, 3].iter()[3:5].size_hint(), content="Some(0)")
+ debug_inspect([|1, 2, 3|][1:3].size_hint(), content="Some(2)")
+ debug_inspect([|1, 2, 3|][3:5].size_hint(), content="Some(0)")
debug_inspect(
Iter::new(() => Some(1)).view(start=2, end=1).size_hint(),
content="Some(0)",
@@ -351,18 +341,12 @@ test "size_hint" {
Iter::new(() => Some(1)).view(start=1, end=3).size_hint(),
content="None",
)
- debug_inspect([1, 2, 3].iter().filter(x => x > 1).size_hint(), content="None")
- debug_inspect(
- [1, 2, 3]
- .iter()
- .filter_map(x => if x > 1 { Some(x) } else { None })
- .size_hint(),
- content="None",
- )
+ debug_inspect([|1, 2, 3|].filter(x => x > 1).size_hint(), content="None")
debug_inspect(
- [1, 2, 3].iter().take_while(x => x < 3).size_hint(),
+ [|1, 2, 3|].filter_map(x => if x > 1 { Some(x) } else { None }).size_hint(),
content="None",
)
+ debug_inspect([|1, 2, 3|].take_while(x => x < 3).size_hint(), content="None")
let unknown : Iter[Int] = Iter::new(() => None)
let known_empty : Iter[Int] = Iter::new(() => None, size_hint=0)
debug_inspect(unknown.size_hint(), content="None")
@@ -408,24 +392,26 @@ test "filter_map" {
debug_inspect(r1, content="[3, 4, 5]")
let r2 : Array[Unit] = arr.iter().filter_map(_x => None).collect()
debug_inspect(r2, content="[]")
- let r3 : Array[Unit] = [].iter().filter_map(x => Some(x)).collect()
+ let r3 : Array[Unit] = [||].filter_map(x => Some(x)).collect()
debug_inspect(r3, content="[]")
// Test using next() directly to ensure the closure is executed
- let it1 = [1, 2, 3, 4, 5]
- .iter()
- .filter_map(x => if x % 2 == 0 { Some(x * 10) } else { None })
+ let it1 = [|1, 2, 3, 4, 5|].filter_map(x => {
+ if x % 2 == 0 {
+ Some(x * 10)
+ } else {
+ None
+ }
+ })
debug_inspect(it1.next(), content="Some(20)")
debug_inspect(it1.next(), content="Some(40)")
debug_inspect(it1.next(), content="None")
// Test None branch inside while loop
- let it2 = [1, 2, 3]
- .iter()
- .filter_map(x => if x == 2 { None } else { Some(x) })
+ let it2 = [|1, 2, 3|].filter_map(x => if x == 2 { None } else { Some(x) })
debug_inspect(it2.next(), content="Some(1)")
debug_inspect(it2.next(), content="Some(3)")
debug_inspect(it2.next(), content="None")
// Test iter exhaustion (else branch)
- let it3 : Iter[Unit] = [1].iter().filter_map(_ => None)
+ let it3 : Iter[Unit] = [|1|].filter_map(_ => None)
debug_inspect(it3.next(), content="None")
}
@@ -441,7 +427,7 @@ test "flat_map" {
test "flat_map2" {
let iter = test_from_array(['1', '2', '3', '4', '5'])
let exb = StringBuilder(size_hint=0)
- iter.flat_map(x => Iter::singleton(x)).each(x => exb.write_char(x))
+ iter.flat_map(x => [|x|]).each(x => exb.write_char(x))
inspect(exb, content="12345")
}
@@ -489,7 +475,7 @@ test "concat" {
///|
test "zip" {
let numbers = (1).until(6)
- let letters = ["a", "b", "c"].iter()
+ let letters = [|"a", "b", "c"|]
debug_inspect(
numbers.zip(letters).collect(),
content=(
@@ -501,7 +487,7 @@ test "zip" {
///|
test "zip alias combine" {
let numbers = (1).until(4)
- let letters = ["x", "y", "z", "w"].iter()
+ let letters = [|"x", "y", "z", "w"|]
debug_inspect(
numbers.combine(letters).collect(),
content=(
@@ -515,7 +501,7 @@ test "zip short-circuits when either side ends" {
let lhs_evaluated = []
let rhs_evaluated = []
let lhs = (1).until(10).tap(x => lhs_evaluated.push(x))
- let rhs = [10, 20].iter().tap(x => rhs_evaluated.push(x))
+ let rhs = [|10, 20|].tap(x => rhs_evaluated.push(x))
debug_inspect(lhs.zip(rhs).collect(), content="[(1, 10), (2, 20)]")
debug_inspect(lhs_evaluated, content="[1, 2, 3]")
debug_inspect(rhs_evaluated, content="[10, 20]")
@@ -590,7 +576,7 @@ test "until" {
debug_inspect(
(@int.MAX_VALUE - 1)
.until(@int.MAX_VALUE, inclusive=true)
- .concat([0].iter())
+ .concat([|0|])
.to_array(),
content=(
#|[2147483646, 2147483647, 0]
@@ -778,7 +764,7 @@ test "intersperse early end" {
///|
test "intersperse stops on separator" {
- let iter = [1, 2].iter().intersperse(0)
+ let iter = [|1, 2|].intersperse(0)
let seen = []
for x in iter {
seen.push(x)
@@ -859,25 +845,25 @@ test "tree" {
///|
test "Iter::intersperse" {
debug_inspect(
- [1, 2, 3].iter().intersperse(0).to_array(),
+ [|1, 2, 3|].intersperse(0).to_array(),
content=(
#|[1, 0, 2, 0, 3]
),
)
debug_inspect(
- [1, 2, 3, 4, 5, 6, 7, 8, 9].iter().intersperse(0).to_array(),
+ [|1, 2, 3, 4, 5, 6, 7, 8, 9|].intersperse(0).to_array(),
content=(
#|[1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9]
),
)
debug_inspect(
- ([] : Array[Int]).iter().intersperse(0).to_array(),
+ ([||] : Iter[Int]).intersperse(0).to_array(),
content=(
#|[]
),
)
debug_inspect(
- [1].iter().intersperse(0).to_array(),
+ [|1|].intersperse(0).to_array(),
content=(
#|[1]
),
@@ -886,40 +872,40 @@ test "Iter::intersperse" {
///|
test "Iter::last" {
- debug_inspect([1, 2, 3].iter().last(), content="Some(3)")
- debug_inspect([1].iter().last(), content="Some(1)")
- debug_inspect(([] : Array[Int]).iter().last(), content="None")
+ debug_inspect([|1, 2, 3|].last(), content="Some(3)")
+ debug_inspect([|1|].last(), content="Some(1)")
+ debug_inspect(([||] : Iter[Int]).last(), content="None")
}
///|
test "Iter::head" {
- debug_inspect([1, 2, 3].iter().head(), content="Some(1)")
- debug_inspect([1].iter().head(), content="Some(1)")
- debug_inspect(([] : Array[Int]).iter().head(), content="None")
+ debug_inspect([|1, 2, 3|].head(), content="Some(1)")
+ debug_inspect([|1|].head(), content="Some(1)")
+ debug_inspect(([||] : Iter[Int]).head(), content="None")
}
///|
test "Iter::as_view" {
debug_inspect(
- [1, 2, 3].iter()[1:2].to_array(),
+ [|1, 2, 3|][1:2].to_array(),
content=(
#|[2]
),
)
debug_inspect(
- [1, 2, 3].iter()[1:].to_array(),
+ [|1, 2, 3|][1:].to_array(),
content=(
#|[2, 3]
),
)
debug_inspect(
- [1, 2, 3].iter()[1:].to_array(),
+ [|1, 2, 3|][1:].to_array(),
content=(
#|[2, 3]
),
)
debug_inspect(
- [1, 2, 3].iter()[:].to_array(),
+ [|1, 2, 3|][:].to_array(),
content=(
#|[1, 2, 3]
),
@@ -938,69 +924,69 @@ test "Iter::enumerate" {
///|
test "next function - basic functionality" {
- let iter = Iter::singleton(42)
+ let iter = [|42|]
debug_inspect(iter.next(), content="Some(42)")
}
///|
test "next function - empty iter" {
- let iter : Iter[Int] = Iter::empty()
+ let iter : Iter[Int] = [||]
debug_inspect(iter.next(), content="None")
}
///|
test "next function - multiple elements" {
- let iter = [1, 2, 3].iter()
+ let iter = [|1, 2, 3|]
debug_inspect(iter.next(), content="Some(1)")
}
///|
test "next function - random cases" {
- let iter1 = [10, 20, 30].iter()
+ let iter1 = [|10, 20, 30|]
debug_inspect(iter1.next(), content="Some(10)")
- let iter2 = [-5, 0, 5].iter()
+ let iter2 = [|-5, 0, 5|]
debug_inspect(iter2.next(), content="Some(-5)")
- let iter3 = [100, 200, 300, 400].iter()
+ let iter3 = [|100, 200, 300, 400|]
debug_inspect(iter3.next(), content="Some(100)")
- let iter4 = [-10, -20, -30].iter()
+ let iter4 = [|-10, -20, -30|]
debug_inspect(iter4.next(), content="Some(-10)")
- let iter5 = [0, 0, 0].iter()
+ let iter5 = [|0, 0, 0|]
debug_inspect(iter5.next(), content="Some(0)")
}
///|
test "@builtin.join/empty_iter" {
- let empty_iter : Iter[String] = Iter::empty()
+ let empty_iter : Iter[String] = [||]
inspect(empty_iter.join(""), content="")
}
///|
test "@builtin.join/single_element" {
- let single_elem_iter : Iter[String] = Iter::singleton("Test")
+ let single_elem_iter : Iter[String] = [|"Test"|]
inspect(single_elem_iter.join(""), content="Test")
}
///|
test "@builtin.join/multiple_elements_with_separator" {
- let iter : Iter[String] = ["A", "B", "C"].iter()
+ let iter : Iter[String] = [|"A", "B", "C"|]
inspect(iter.join(","), content="A,B,C")
}
///|
test "@builtin.join/multiple_elements_without_separator" {
- let iter : Iter[String] = ["A", "B", "C"].iter()
+ let iter : Iter[String] = [|"A", "B", "C"|]
inspect(iter.join(""), content="ABC")
}
///|
test "@builtin.join/any_to_string_view_element" {
- let iter : Iter[StringView] = ["a"[:], "b", "c"].iter()
+ let iter : Iter[StringView] = [|"a", "b", "c"|]
inspect(iter.join("-"), content="a-b-c")
}
///|
test "Iter::nth" {
- let it = () => [1, 2, 3, 4, 5].iter()
+ let it = () => [|1, 2, 3, 4, 5|]
debug_inspect(it().nth(2), content="Some(3)")
debug_inspect(it().nth(4), content="Some(5)")
debug_inspect(it().nth(5), content="None")
@@ -1016,63 +1002,63 @@ test "Iter::nth" {
///|
test "@builtin.Iter::maximum" {
// Basic functionality with integers
- debug_inspect([1, 2, 3, 4, 5].iter().maximum(), content="Some(5)")
+ debug_inspect([|1, 2, 3, 4, 5|].maximum(), content="Some(5)")
// With negative numbers
- debug_inspect([-5, -3, -1, -10].iter().maximum(), content="Some(-1)")
+ debug_inspect([|-5, -3, -1, -10|].maximum(), content="Some(-1)")
// Single element
- debug_inspect([42].iter().maximum(), content="Some(42)")
+ debug_inspect([|42|].maximum(), content="Some(42)")
// Empty iter should return None
- let empty_iter : Iter[Int] = Iter::empty()
+ let empty_iter : Iter[Int] = [||]
debug_inspect(empty_iter.maximum(), content="None")
}
///|
test "@builtin.Iter::minimum" {
// Test with normal sequence
- let arr = [3, 1, 4, 1, 5].iter()
+ let arr = [|3, 1, 4, 1, 5|]
debug_inspect(arr.minimum(), content="Some(1)")
// Test with single element
- let single = [42].iter()
+ let single = [|42|]
debug_inspect(single.minimum(), content="Some(42)")
// Test with empty sequence
- let empty : Iter[Int] = Iter::empty()
+ let empty : Iter[Int] = [||]
debug_inspect(empty.minimum(), content="None")
}
///|
test "Iter::intersperse with early termination" {
- let iter = [1, 2, 3].iter()
+ let iter = [|1, 2, 3|]
let result = iter.intersperse(0).take(3).collect()
debug_inspect(result, content="[1, 0, 2]")
}
///|
test "Iter::iter method" {
- let original = [1, 2, 3].iter()
+ let original = [|1, 2, 3|]
let result = original.iter().collect()
debug_inspect(result, content="[1, 2, 3]")
}
///|
test "Iter::contains method" {
- let iter = [1, 2, 3, 4, 5].iter()
+ let iter = [|1, 2, 3, 4, 5|]
inspect(iter.contains(3), content="true")
inspect(iter.contains(6), content="false")
}
///|
test "Iter::flatten method" {
- let nested = [[1, 2], [3, 4], [5, 6]].iter().map(x => x.iter())
+ let nested = [|[1, 2], [3, 4], [5, 6]|].map(x => x.iter())
let flattened = nested.flatten().collect()
debug_inspect(flattened, content="[1, 2, 3, 4, 5, 6]")
}
///|
test "Iter::add operator" {
- let iter1 = [1, 2, 3].iter()
- let iter2 = [4, 5, 6].iter()
+ let iter1 = [|1, 2, 3|]
+ let iter2 = [|4, 5, 6|]
let result = (iter1 + iter2).collect()
debug_inspect(result, content="[1, 2, 3, 4, 5, 6]")
}
@@ -1128,7 +1114,7 @@ test "Float::until negative step" {
///|
// test "group_by with consecutive identical elements" {
-// let iter = [1, 1, 2, 2, 3, 3].iter()
+// let iter = [|1, 1, 2, 2, 3, 3|]
// let grouped = iter.group_by((x) => { x })
// @test.assert_eq(grouped.get(1), Some([1, 1]))
// @test.assert_eq(grouped.get(2), Some([2, 2]))
@@ -1137,7 +1123,7 @@ test "Float::until negative step" {
///|
// test "group_by with non-consecutive identical elements" {
-// let iter = [1, 2, 1, 3, 2, 1].iter()
+// let iter = [|1, 2, 1, 3, 2, 1|]
// let grouped = iter.group_by((x) => { x })
// @test.assert_eq(grouped.get(1), Some([1, 1, 1]))
// @test.assert_eq(grouped.get(2), Some([2, 2]))
@@ -1146,21 +1132,21 @@ test "Float::until negative step" {
///|
// test "group_by with empty input" {
-// let iter : Iter[Int] = Iter::empty()
+// let iter : Iter[Int] = [||]
// let grouped = iter.group_by((x) => { x })
// @test.assert_eq(grouped.length(), 0)
// }
///|
// test "group_by with single element input" {
-// let iter = [42].iter()
+// let iter = [|42|]
// let grouped = iter.group_by((x) => { x })
// @test.assert_eq(grouped.get(42), Some([42]))
// }
///|
// test "group_by with custom key function" {
-// let iter = [1, 2, 3, 4].iter()
+// let iter = [|1, 2, 3, 4|]
// let grouped = iter.group_by((x) => { x % 2 })
// @test.assert_eq(grouped.get(0), Some([2, 4]))
// @test.assert_eq(grouped.get(1), Some([1, 3]))
@@ -1168,7 +1154,7 @@ test "Float::until negative step" {
///|
// test "group_by with strings" {
-// let iter = ["apple", "avocado", "banana", "cherry", "blueberry"].iter()
+// let iter = [|"apple", "avocado", "banana", "cherry", "blueberry"|]
// let grouped = iter.group_by((s) => { s.charcode_at(0) })
// @test.assert_eq(grouped.get('a'), Some(["apple", "avocado"]))
// @test.assert_eq(grouped.get('b'), Some(["banana", "blueberry"]))
@@ -1181,13 +1167,13 @@ test "Float::until negative step" {
// name : String
// age : Int
// }
-// let people = [
+// let people = [|
// Person::{ name: "Alice", age: 25 },
// Person::{ name: "Bob", age: 25 },
// Person::{ name: "Charlie", age: 30 },
// Person::{ name: "Dave", age: 35 },
// Person::{ name: "Eve", age: 30 },
-// ].iter()
+// |]
// let grouped = people.group_by((p) => { p.age })
// let groups = grouped.values().map((a) => { a.map((p) => { p.name }) }).collect()
// @test.assert_eq(groups, [["Alice", "Bob"], ["Charlie", "Eve"], ["Dave"]])
@@ -1195,15 +1181,15 @@ test "Float::until negative step" {
///|
test "iter2" {
- let iter : Iter[Int] = [].iter()
+ let iter : Iter[Int] = [||]
for _, _ in iter.iter2() {
assert_true(false)
}
- let iter = [0, 1, 2].iter().iter2()
+ let iter = [|0, 1, 2|].iter2()
while iter.next() is Some((i, x)) {
@test.assert_eq(i, x)
}
- let iter = [0, 1, 2].iter()
+ let iter = [|0, 1, 2|]
for i, x in iter.iter2() {
@test.assert_eq(i, x)
}
@@ -1211,7 +1197,7 @@ test "iter2" {
///|
test "Iter::iter2" {
- let iter = [0, 1, 2].iter()
+ let iter = [|0, 1, 2|]
for i, x in iter.iter2() {
@test.assert_eq(i, x)
}
@@ -1222,41 +1208,41 @@ test "Iter::iter2" {
///|
test "Iter::to_json and any on empty" {
- let iter = [1, 2, 3].iter()
+ let iter = [|1, 2, 3|]
@json.json_inspect(ToJson::to_json(iter), content=[1, 2, 3])
- let empty : Iter[Int] = Iter::empty()
+ let empty : Iter[Int] = [||]
inspect(empty.any(_ => true), content="false")
}
///|
test "Iter::map_while empty and last" {
- let empty : Iter[Int] = Iter::empty()
+ let empty : Iter[Int] = [||]
debug_inspect(empty.map_while(_ => Some(1)).collect(), content="[]")
- debug_inspect([1, 2, 3].iter().last(), content="Some(3)")
- let empty_last : Iter[Int] = Iter::empty()
+ debug_inspect([|1, 2, 3|].last(), content="Some(3)")
+ let empty_last : Iter[Int] = [||]
debug_inspect(empty_last.last(), content="None")
}
///|
test "Iter::maximum" {
- debug_inspect([1, 3, 2].iter().maximum(), content="Some(3)")
- let empty : Iter[Int] = Iter::empty()
+ debug_inspect([|1, 3, 2|].maximum(), content="Some(3)")
+ let empty : Iter[Int] = [||]
debug_inspect(empty.maximum(), content="None")
}
///|
test "Iter::view variants" {
- debug_inspect([1, 2, 3].iter().view(start=-1).collect(), content="[1, 2, 3]")
- debug_inspect([1, 2, 3].iter()[:2].collect(), content="[1, 2]")
- debug_inspect([1, 2, 3].iter()[1:].collect(), content="[2, 3]")
- debug_inspect([1, 2, 3, 4].iter()[1:3].collect(), content="[2, 3]")
- debug_inspect([1, 2, 3].iter()[2:2].collect(), content="[]")
- debug_inspect([1, 2, 3].iter()[1:0].collect(), content="[]")
+ debug_inspect([|1, 2, 3|].view(start=-1).collect(), content="[1, 2, 3]")
+ debug_inspect([|1, 2, 3|][:2].collect(), content="[1, 2]")
+ debug_inspect([|1, 2, 3|][1:].collect(), content="[2, 3]")
+ debug_inspect([|1, 2, 3, 4|][1:3].collect(), content="[2, 3]")
+ debug_inspect([|1, 2, 3|][2:2].collect(), content="[]")
+ debug_inspect([|1, 2, 3|][1:0].collect(), content="[]")
}
///|
test "Iter::iter and Iter2 conversions" {
- debug_inspect([1, 2].iter().iter().collect(), content="[1, 2]")
+ debug_inspect([|1, 2|].iter().collect(), content="[1, 2]")
let pairs = ['a', 'b']
.iter2()
.iter()
diff --git a/builtin/iterator.mbt b/builtin/iterator.mbt
index eff999309e..2336b5e202 100644
--- a/builtin/iterator.mbt
+++ b/builtin/iterator.mbt
@@ -63,15 +63,14 @@ pub impl[X : Show] Show for Iter[X]
///|
pub impl[X : Show] Show for Iter[X] with fn output(self, logger) {
- logger.write_string("[")
+ logger <+ "["
if self.next() is Some(x) {
logger.write_object(x)
while self.next() is Some(x) {
- logger.write_string(", ")
- logger.write_object(x)
+ logger <+ ", \{cb => cb.write_object(x)}"
}
}
- logger.write_string("]")
+ logger <+ "]"
}
///|
@@ -245,12 +244,14 @@ pub fn[X] Iter::new(f : () -> X?, size_hint? : Int) -> Iter[X] {
Some(_) => Some(0)
None => None
}
- { f, size_hint }
+ { f, size_hint, }
}
///|
/// Creates an empty iterator.
///
+/// Prefer the iterator literal `[||]`, which is equivalent and shorter.
+///
/// # Type Parameters
///
/// - `X`: The type of the elements in the iterator.
@@ -265,6 +266,8 @@ pub fn[X] Iter::empty() -> Iter[X] {
///|
/// Creates an iterator that contains a single element.
///
+/// Prefer the iterator literal `[|elem|]`, which is equivalent and shorter.
+///
/// # Type Parameters
///
/// - `X`: The type of the element in the iterator.
@@ -275,7 +278,7 @@ pub fn[X] Iter::empty() -> Iter[X] {
///
/// # Returns
///
-/// Returns an iterator of type `Iter[X]` that contains the single element `a`.
+/// Returns an iterator of type `Iter[X]` that contains the single element `elem`.
pub fn[X] Iter::singleton(elem : X) -> Iter[X] {
let mut consumed = false
Iter::new(
@@ -323,7 +326,7 @@ pub fn[X] Iter::repeat(x : X) -> Iter[X] {
///
/// # Returns
///
-/// A new iterator that only contains the elements for which the predicate function returns `IterContinue`.
+/// A new iterator that only contains the elements for which the predicate function returns `true`.
///
/// # Note
/// The old iterator `self` must not be used again after calling `filter`.
@@ -443,7 +446,7 @@ pub fn[X, Y] Iter::filter_map(self : Iter[X], f : (X) -> Y?) -> Iter[Y] {
/// The old iterator `self` and the iterators returned by `f`
/// must not be used again after calling `flat_map`.
pub fn[X, Y] Iter::flat_map(self : Iter[X], f : (X) -> Iter[Y]) -> Iter[Y] {
- let mut current_iter = Some(Iter::empty())
+ let mut current_iter = Some([||])
Iter::new(fn() {
guard current_iter is Some(iter) else { None }
for x = iter.next() {
@@ -721,7 +724,7 @@ pub fn[X] Iter::find_first(self : Iter[X], f : (X) -> Bool) -> X? {
/// Returns a new iterator that contains the elements of `self` followed by the elements of `other`.
///
/// # Note
-/// The old iterator `self` and `other` must not be used again after calling `tap`.
+/// The old iterators `self` and `other` must not be used again after calling `concat`.
pub fn[X] Iter::concat(self : Iter[X], other : Iter[X]) -> Iter[X] {
let mut in_first = true
let size_hint = match (self.size_hint, other.size_hint) {
@@ -771,7 +774,7 @@ pub fn[X] Iter::concat(self : Iter[X], other : Iter[X]) -> Iter[X] {
/// ```mbt check
/// test {
/// let numbers = (1).until(5)
-/// let letters = ["a", "b", "c"].iter()
+/// let letters = [|"a", "b", "c"|]
/// debug_inspect(
/// numbers.zip(letters).collect(),
/// content="[(1, \"a\"), (2, \"b\"), (3, \"c\")]",
@@ -809,7 +812,7 @@ pub impl[T] Add for Iter[T] with fn add(self, other) {
#alias(collect)
pub fn[X] Iter::to_array(self : Iter[X]) -> Array[X] {
let result = match self.size_hint {
- Some(n) => Array::new(capacity=n)
+ Some(n) => Array::Array(capacity=n)
None => []
}
while self.next() is Some(x) {
@@ -868,7 +871,7 @@ pub fn[X] Iter::last(self : Iter[X]) -> X? {
/// ```mbt check
/// test {
/// let arr = []
-/// [1, 2, 3].iter().intersperse(0).each(i => arr.push(i))
+/// [|1, 2, 3|].intersperse(0).each(i => arr.push(i))
/// @test.assert_eq(arr, [1, 0, 2, 0, 3])
/// }
/// ```
@@ -968,10 +971,10 @@ pub fn[X] Iter::view(self : Iter[X], start? : Int = 0, end? : Int) -> Iter[X] {
///
/// ```mbt check
/// test {
-/// let iter = [1, 2, 3, 4, 5].iter()
+/// let iter = [|1, 2, 3, 4, 5|]
/// inspect(iter.contains(3), content="true")
/// inspect(iter.contains(6), content="false")
-/// let iter = Iter::empty()
+/// let iter = [||]
/// inspect(iter.contains(1), content="false")
/// }
/// ```
@@ -989,8 +992,8 @@ pub fn[X : Eq] Iter::contains(self : Iter[X], value : X) -> Bool {
}
///|
-/// Returns the nth element of the iterator, or `None` if the iterator is
-/// shorter than `n` elements.
+/// Returns the `n`-th element of the iterator, counting from zero, or `None` if
+/// `n` is negative or the iterator has fewer than `n + 1` elements.
/// The iterator `self` will advance past the returned element.
pub fn[X] Iter::nth(self : Iter[X], n : Int) -> X? {
guard n >= 0 else { None }
@@ -1044,7 +1047,7 @@ pub fn[X, Y] Iter2::new(f : () -> (X, Y)?, size_hint? : Int) -> Iter2[X, Y] {
Some(_) => Some(0)
None => None
}
- Iter2({ f, size_hint })
+ Iter2({ f, size_hint, })
}
///|
@@ -1069,16 +1072,6 @@ pub fn[X, Y] Iter2::next(self : Iter2[X, Y]) -> (X, Y)? {
self.0.next()
}
-///|
-#deprecated("Use Debug instead of Show for debugging purposes. See https://github.com/moonbitlang/core/blob/main/debug/README.mbt.md")
-pub impl[X : Show, Y : Show] Show for Iter2[X, Y]
-
-///|
-#warnings("-deprecated")
-pub impl[X : Show, Y : Show] Show for Iter2[X, Y] with fn output(self, logger) {
- self.0.output(logger)
-}
-
///|
/// Apply callback to each pair.
pub fn[X, Y] Iter2::each(self : Iter2[X, Y], f : (X, Y) -> Unit) -> Unit {
diff --git a/builtin/json.mbt b/builtin/json.mbt
index f55d863afb..888836ad49 100644
--- a/builtin/json.mbt
+++ b/builtin/json.mbt
@@ -36,9 +36,7 @@ pub enum Json {
///|
pub impl Eq for Json with fn equal(a, b) {
match (a, b) {
- (Null, Null) => true
- (True, True) => true
- (False, False) => true
+ (Null, Null) | (True, True) | (False, False) => true
(Number(a_num, ..), Number(b_num, ..)) => a_num == b_num
(String(a_str), String(b_str)) => a_str == b_str
(Array(a_arr), Array(b_arr)) => a_arr == b_arr
@@ -293,7 +291,7 @@ pub impl[X : ToJson] ToJson for FixedArray[X] with fn to_json(self) {
if len == 0 {
return []
}
- let res = Array::make_uninit(self.length())
+ let res = Array::unsafe_make_uninit(self.length())
for i, x in self {
res.unsafe_set(i, ToJson::to_json(x))
}
@@ -306,7 +304,7 @@ pub impl[X : ToJson] ToJson for ArrayView[X] with fn to_json(self) {
if len == 0 {
return []
}
- let res = Array::make_uninit(self.length())
+ let res = Array::unsafe_make_uninit(self.length())
for i, x in self {
res.unsafe_set(i, ToJson::to_json(x))
}
diff --git a/builtin/linked_hash_map.mbt b/builtin/linked_hash_map.mbt
index 815518b7cb..63628f82b2 100644
--- a/builtin/linked_hash_map.mbt
+++ b/builtin/linked_hash_map.mbt
@@ -51,6 +51,29 @@ struct Map[K, V] {
// Implementations
+// SAFETY NOTE for the unchecked probe accesses in this file.
+//
+// `capacity` is always a power of two and at least 1, `capacity_mask` is
+// `capacity - 1`, and `entries.length()` is `capacity`. A probe index is
+// therefore always in bounds: it starts as `hash & capacity_mask` and each
+// step re-masks with `(idx + 1) & capacity_mask`. `grow` installs the new
+// array, capacity and mask together before any rehash probe runs, and
+// `set_with_hash` re-masks from scratch after growing.
+//
+// Only those probe indices use `unsafe_get` / `unsafe_set`. `Map` also
+// stores slot indices in the list itself -- `prev`, `tail`, and the index
+// `retain` destructures out of an entry -- and every access through one of
+// those keeps the checked form. They are in bounds too, being former probe
+// indices in a table that never shrinks, but that argument depends on the
+// list being maintained correctly rather than on arithmetic alone, so it is
+// not one to build unchecked access on.
+//
+// `shift_back` is the exception, and deliberately so: it is reachable from
+// `retain` with a stored index, but `retain` performs its own checked read
+// immediately before calling, so `shift_back` has a local caller-based proof
+// that does not depend on the list invariant. It carries that proof at its
+// definition, and it is what makes its `set_entry` call sound.
+
///|
let default_init_capacity = 8
@@ -140,8 +163,9 @@ fn[K : Eq, V] Map::set_with_hash(
hash : Int,
) -> Unit {
// Only grow when actually inserting a new entry, not when updating existing
+ // SAFETY: masked probe index; see the note at the top of this file.
for psl = 0, idx = hash & self.capacity_mask {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
None => {
// Need to insert new entry - check if grow is needed first
if self.size >= self.grow_at {
@@ -149,7 +173,7 @@ fn[K : Eq, V] Map::set_with_hash(
// Restart search with new capacity_mask
continue 0, hash & self.capacity_mask
}
- let entry = { prev: self.tail, next: None, psl, key, value, hash }
+ let entry = { prev: self.tail, next: None, psl, key, value, hash, }
self.add_entry_to_tail(idx, entry)
return
}
@@ -167,7 +191,7 @@ fn[K : Eq, V] Map::set_with_hash(
continue 0, hash & self.capacity_mask
}
self.push_away(idx, curr_entry)
- let entry = { prev: self.tail, next: None, psl, key, value, hash }
+ let entry = { prev: self.tail, next: None, psl, key, value, hash, }
self.add_entry_to_tail(idx, entry)
return
}
@@ -184,8 +208,9 @@ fn[K, V] Map::push_away(
idx : Int,
entry : Entry[K, V],
) -> Unit {
+ // SAFETY: masked probe index; see the note at the top of this file.
for psl = entry.psl + 1, idx = (idx + 1) & self.capacity_mask, entry = entry {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
None => {
entry.psl = psl
self.set_entry(entry, idx)
@@ -219,7 +244,7 @@ fn[K, V] Map::set_entry(
None => self.tail = new_idx
Some(next) => next.prev = new_idx
}
- self.entries[new_idx] = Some(entry)
+ self.entries.unsafe_set(new_idx, Some(entry))
}
///|
@@ -243,8 +268,9 @@ fn[K, V] Map::set_entry(
/// ```
pub fn[K : Hash + Eq, V] Map::get(self : Map[K, V], key : K) -> V? {
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & self.capacity_mask {
- guard self.entries[idx] is Some(entry) else { break None }
+ guard self.entries.unsafe_get(idx) is Some(entry) else { break None }
if entry.hash == hash && entry.key == key {
break Some(entry.value)
}
@@ -260,8 +286,9 @@ pub fn[K : Hash + Eq, V] Map::get(self : Map[K, V], key : K) -> V? {
#alias("_[_]")
pub fn[K : Hash + Eq, V] Map::at(self : Map[K, V], key : K) -> V {
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & self.capacity_mask {
- guard! self.entries[idx] is Some(entry)
+ guard! self.entries.unsafe_get(idx) is Some(entry)
if entry.hash == hash && entry.key == key {
return entry.value
}
@@ -299,8 +326,9 @@ pub fn[K : Hash + Eq, V] Map::get_or_default(
default : V,
) -> V {
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & self.capacity_mask {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
Some(entry) => {
if entry.hash == hash && entry.key == key {
break entry.value
@@ -323,9 +351,10 @@ pub fn[K : Hash + Eq, V] Map::get_or_init(
default : () -> V,
) -> V {
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
let (idx, psl, new_value, push_away) = for psl = 0, idx = hash &
self.capacity_mask {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
Some(entry) => {
if entry.hash == hash && entry.key == key {
return entry.value
@@ -389,8 +418,9 @@ pub fn[K : Hash + Eq, V] Map::update_or_default(
f : (V) -> V,
) -> Unit {
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
let (idx, psl, push_away) = for psl = 0, idx = hash & self.capacity_mask {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
Some(entry) => {
if entry.hash == hash && entry.key == key {
entry.value = f(entry.value)
@@ -411,7 +441,7 @@ pub fn[K : Hash + Eq, V] Map::update_or_default(
if push_away is Some(entry) {
self.push_away(idx, entry)
}
- let entry = { prev: self.tail, next: None, psl, hash, key, value: default }
+ let entry = { prev: self.tail, next: None, psl, hash, key, value: default, }
self.add_entry_to_tail(idx, entry)
}
}
@@ -421,8 +451,9 @@ pub fn[K : Hash + Eq, V] Map::update_or_default(
pub fn[K : Hash + Eq, V] Map::contains(self : Map[K, V], key : K) -> Bool {
// inline Map::get to avoid boxing
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & self.capacity_mask {
- guard self.entries[idx] is Some(entry) else { break false }
+ guard self.entries.unsafe_get(idx) is Some(entry) else { break false }
if entry.hash == hash && entry.key == key {
break true
}
@@ -462,8 +493,9 @@ pub fn[K : Hash + Eq, V : Eq] Map::contains_kv(
) -> Bool {
// inline Map::get to avoid boxing
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & self.capacity_mask {
- guard self.entries[idx] is Some(entry) else { break false }
+ guard self.entries.unsafe_get(idx) is Some(entry) else { break false }
if entry.hash == hash && entry.key == key && entry.value == value {
break true
}
@@ -505,8 +537,9 @@ fn[K : Eq, V] Map::remove_with_hash(
key : K,
hash : Int,
) -> Unit {
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & self.capacity_mask {
- guard self.entries[idx] is Some(entry) else { break }
+ guard self.entries.unsafe_get(idx) is Some(entry) else { break }
if entry.hash == hash && entry.key == key {
self.remove_entry(entry)
self.shift_back(idx)
@@ -532,7 +565,7 @@ fn[K, V] Map::add_entry_to_tail(
tail => self.entries[tail].unwrap().next = Some(entry)
}
self.tail = idx
- self.entries[idx] = Some(entry)
+ self.entries.unsafe_set(idx, Some(entry))
self.size += 1
}
@@ -550,11 +583,17 @@ fn[K, V] Map::remove_entry(self : Map[K, V], entry : Entry[K, V]) -> Unit {
///|
fn[K, V] Map::shift_back(self : Map[K, V], idx : Int) -> Unit {
+ // SAFETY: the initial `cur` is in bounds by every route its callers take,
+ // and none of them requires trusting the list invariant: `remove` and
+ // `update` pass a masked probe index, and `retain` performs its own
+ // checked `entries[idx]` read immediately before calling. `next` is
+ // re-masked each step and later `cur` values are previous `next` values.
+ // This is also what makes the `set_entry` call below sound.
for cur = idx {
let next = (cur + 1) & self.capacity_mask
- match self.entries[next] {
+ match self.entries.unsafe_get(next) {
None | Some({ psl: 0, .. }) => {
- self.entries[cur] = None
+ self.entries.unsafe_set(cur, None)
break
}
Some(entry) => {
@@ -594,8 +633,9 @@ fn[K, V] Map::grow(self : Map[K, V]) -> Unit {
#owned(outer)
fn[K, V] Map::rehash_place_entry(self : Map[K, V], outer : Entry[K, V]) -> Unit {
let hash = outer.hash
+ // SAFETY: masked probe index; see the note at the top of this file.
for psl = 0, idx = hash & self.capacity_mask {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
None => {
outer.psl = psl
outer.prev = self.tail
@@ -629,17 +669,16 @@ pub impl[K : Show, V : Show] Show for Map[K, V]
///|
pub impl[K : Show, V : Show] Show for Map[K, V] with fn output(self, logger) {
- logger.write_string("{")
+ logger <+ "{"
for x = 0, y = self.head {
match (x, y) {
- (_, None) => break logger.write_string("}")
+ (_, None) => break logger <+ "}"
(i, Some({ key, value, next, .. })) => {
if i > 0 {
- logger.write_string(", ")
+ logger <+ ", "
}
- logger.write_object(key)
- logger.write_string(": ")
- logger.write_object(value)
+ logger <+
+ "\{cb => cb.write_object(key)}: \{cb => cb.write_object(value)}"
continue i + 1, next
}
}
@@ -776,7 +815,7 @@ pub fn[K, V] Map::values(self : Map[K, V]) -> Iter[V] {
///|
/// Converts the hash map to an array.
pub fn[K, V] Map::to_array(self : Map[K, V]) -> Array[(K, V)] {
- let arr = Array::make_uninit(self.size)
+ let arr = Array::unsafe_make_uninit(self.size)
let mut i = 0
for x = self.head {
match x {
@@ -853,7 +892,7 @@ pub fn[K, V, V2] Map::map(self : Map[K, V], f : (K, V) -> V2) -> Map[K, V2] {
for entry = last, idx = self.tail, next = (None : Entry[K, V2]?) {
let { prev, psl, hash, key, value, .. } = entry
let new_value = f(key, value)
- let new_entry = { prev, next, psl, hash, key, value: new_value }
+ let new_entry = { prev, next, psl, hash, key, value: new_value, }
other.entries[idx] = Some(new_entry)
if prev != -1 {
continue self.entries[prev].unwrap(), prev, Some(new_entry)
@@ -885,7 +924,7 @@ pub fn[K, V] Map::copy(self : Map[K, V]) -> Map[K, V] {
guard! self.entries[self.tail] is Some(last)
for entry = last, idx = self.tail, next = (None : Entry[K, V]?) {
let { prev, psl, hash, key, value, .. } = entry
- let new_entry = { prev, next, psl, hash, key, value }
+ let new_entry = { prev, next, psl, hash, key, value, }
other.entries[idx] = Some(new_entry)
if prev != -1 {
continue self.entries[prev].unwrap(), prev, Some(new_entry)
@@ -1094,9 +1133,10 @@ pub fn[K : Hash + Eq, V] Map::update(
f : (V?) -> V?,
) -> Unit {
let hash = Hash::hash(key)
+ // SAFETY: masked probe index; see the note at the top of this file.
let (idx, psl, new_value, push_away) = for psl = 0, idx = hash &
self.capacity_mask {
- match self.entries[idx] {
+ match self.entries.unsafe_get(idx) {
Some(entry) => {
if entry.hash == hash && entry.key == key {
// Found the entry, update its value
@@ -1169,8 +1209,9 @@ pub fn[K : Hash + Eq, V] Map::update(
/// ```
pub fn[V] Map::get_from_bytes(map : Self[Bytes, V], key : BytesView) -> V? {
let hash = key.hash()
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & map.capacity_mask {
- guard map.entries[idx] is Some(entry) else { break None }
+ guard map.entries.unsafe_get(idx) is Some(entry) else { break None }
if entry.hash == hash && key.equal_to_bytes(entry.key) {
break Some(entry.value)
}
@@ -1206,8 +1247,9 @@ pub fn[V] Map::get_from_bytes(map : Self[Bytes, V], key : BytesView) -> V? {
/// ```
pub fn[V] Map::get_from_string(map : Self[String, V], key : StringView) -> V? {
let hash = key.hash()
+ // SAFETY: masked probe index; see the note at the top of this file.
for i = 0, idx = hash & map.capacity_mask {
- guard map.entries[idx] is Some(entry) else { break None }
+ guard map.entries.unsafe_get(idx) is Some(entry) else { break None }
if entry.hash == hash && key.equal_to_string(entry.key) {
break Some(entry.value)
}
diff --git a/builtin/linked_hash_map_bench_test.mbt b/builtin/linked_hash_map_bench_test.mbt
new file mode 100644
index 0000000000..5e4d81e2c5
--- /dev/null
+++ b/builtin/linked_hash_map_bench_test.mbt
@@ -0,0 +1,93 @@
+// 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.
+
+///|
+let map_bench_n = 50000
+
+///|
+test "bench Map::set n=50000" (it : @bench.T) {
+ it.bench(fn() {
+ let m : Map[Int, Int] = Map([])
+ for i in 0.. total = total + v)
+ it.keep(total)
+ })
+}
diff --git a/builtin/linked_hash_map_test.mbt b/builtin/linked_hash_map_test.mbt
index 9fc5537ccf..e2b247f601 100644
--- a/builtin/linked_hash_map_test.mbt
+++ b/builtin/linked_hash_map_test.mbt
@@ -102,7 +102,7 @@ test "Map constructor" {
///|
test "Map::from_iter" {
- let iter = [("a", 1), ("b", 2), ("c", 3)].iter()
+ let iter = [|("a", 1), ("b", 2), ("c", 3)|]
let map = Map::from_iter(iter)
debug_inspect(
map,
diff --git a/builtin/linked_hash_map_wbtest.mbt b/builtin/linked_hash_map_wbtest.mbt
index a279b2c606..79b026a45e 100644
--- a/builtin/linked_hash_map_wbtest.mbt
+++ b/builtin/linked_hash_map_wbtest.mbt
@@ -108,9 +108,9 @@ test "get_or_default" {
///|
test "get_or_init" {
let m : Map[String, Array[Int]] = Map([])
- m.get_or_init("a", () => Array::new()).push(1)
- m.get_or_init("b", () => Array::new()).push(2)
- m.get_or_init("a", () => Array::new()).push(3)
+ m.get_or_init("a", () => Array()).push(1)
+ m.get_or_init("b", () => Array()).push(2)
+ m.get_or_init("a", () => Array()).push(3)
assert_true(m.get("a") == Some([1, 3]))
assert_true(m.get("b") == Some([2]))
assert_true(m.length() == 2)
@@ -120,10 +120,10 @@ test "get_or_init" {
test "get_or_init on push_back" {
let m : Map[MyString, Array[Int]] = Map([], capacity=4)
inspect(m.grow_at, content="3")
- m.set("x", Array::new())
- m.set("xx", Array::new())
+ m.set("x", Array())
+ m.set("xx", Array())
// inspect(m._debug_entries(), content="_,(0,x,[]),(0,xx,[]),_")
- m.get_or_init("a", () => Array::new()).push(1)
+ m.get_or_init("a", () => Array()).push(1)
// inspect(m._debug_entries(), content="_,(0,x,[]),(1,a,[1]),(1,xx,[])")
}
@@ -328,7 +328,7 @@ test "iter" {
.iter()
.each(e => {
let (k, v) = e
- buf.write_string("[\{k}-\{v}]")
+ buf <+ "[\{k}-\{v}]"
})
inspect(buf, content="[1-one][2-two][3-three]")
buf.reset()
@@ -337,7 +337,7 @@ test "iter" {
.take(2)
.each(e => {
let (k, v) = e
- buf.write_string("[\{k}-\{v}]")
+ buf <+ "[\{k}-\{v}]"
})
inspect(buf, content="[1-one][2-two]")
}
@@ -348,7 +348,7 @@ test "iter order" {
m["three"] = 3
m["two"] = 2
let buf = StringBuilder(size_hint=0)
- m.each((k, v) => buf.write_string("[\{k}-\{v}]"))
+ m.each((k, v) => buf <+ "[\{k}-\{v}]")
inspect(buf.to_string(), content="[one-1][three-3][two-2]")
}
@@ -641,8 +641,7 @@ fn[K : Show, V : Show] Map::_debug_entries(self : Map[K, V]) -> String {
}
match entry {
None => buf.write_char('_')
- Some({ psl, key, value, .. }) =>
- buf.write_string("(\{psl},\{key},\{value})")
+ Some({ psl, key, value, .. }) => buf <+ "(\{psl},\{key},\{value})"
}
}
buf.to_string()
diff --git a/builtin/logger_test.mbt b/builtin/logger_test.mbt
index 73af967a4b..c08f32d477 100644
--- a/builtin/logger_test.mbt
+++ b/builtin/logger_test.mbt
@@ -19,13 +19,7 @@ test "logger trait object calls" {
Logger::write_string(logger, "Hello, ")
Logger::write_char(logger, 'w')
&Logger::write_string(logger, "orld!")
- &Logger::write_iter(
- logger,
- [1, 2, 3].iter(),
- prefix="[",
- suffix="]",
- sep="; ",
- )
+ &Logger::write_iter(logger, [|1, 2, 3|], prefix="[", suffix="]", sep="; ")
inspect(
sb,
content=(
diff --git a/builtin/mutarrayview.mbt b/builtin/mutarrayview.mbt
index 9fd5376299..b18dc4d0e5 100644
--- a/builtin/mutarrayview.mbt
+++ b/builtin/mutarrayview.mbt
@@ -244,6 +244,7 @@ pub fn[T] MutArrayView::unsafe_set(
/// inspect(view.length(), content="3") // View contains 3 elements
/// }
/// ```
+#intrinsic("%array.mut_view")
pub fn[T] Array::mut_view(
self : Array[T],
start? : Int = 0,
@@ -291,6 +292,7 @@ pub fn[T] Array::mut_view(
/// }
/// ```
// TODO: rename does not work with docstring test
+#intrinsic("%mutarrayview.mut_view")
pub fn[T] MutArrayView::mut_view(
self : MutArrayView[T],
start? : Int = 0,
@@ -334,6 +336,7 @@ pub fn[T] MutArrayView::mut_view(
/// inspect(view[0], content="2")
/// }
/// ```
+#intrinsic("%fixedarray.mut_view")
pub fn[T] FixedArray::mut_view(
self : FixedArray[T],
start? : Int = 0,
@@ -359,9 +362,11 @@ pub fn[T] FixedArray::mut_view(
/// Parameters:
///
/// * `self` : The mutable array view to create a new view from.
-/// * `start` : The starting index in the array (inclusive). Defaults to 0.
-/// * `end` : The ending index in the array (exclusive). Defaults to the
-/// length of the array.
+/// * `start` : The starting index in the current view (inclusive). Defaults to
+/// 0.
+/// * `end` : The ending index in the current view (exclusive). Defaults to the
+/// length of the current view.
+#intrinsic("%mutarrayview.view")
#alias("_[_:_]")
#alias(sub, deprecated="Use _[_:_] instead")
pub fn[T] MutArrayView::view(
diff --git a/builtin/option.mbt b/builtin/option.mbt
index 13d498b822..398e9eeadc 100644
--- a/builtin/option.mbt
+++ b/builtin/option.mbt
@@ -92,10 +92,8 @@ pub fn[T, Err : Error] Option::unwrap_or_error(
self : T?,
err : Err,
) -> T raise Err {
- match self {
- Some(v) => v
- None => raise err
- }
+ guard self is Some(v) else { raise err }
+ v
}
///|
@@ -109,8 +107,8 @@ pub impl[X] Default for X? with fn default() {
#alias(iterator, deprecated)
pub fn[T] Option::iter(self : T?) -> Iter[T] {
match self {
- Some(v) => Iter::singleton(v)
- None => Iter::empty()
+ Some(v) => [|v|]
+ None => [||]
}
}
diff --git a/builtin/panic_nonjs_test.mbt b/builtin/panic_nonjs_test.mbt
index 0ffa5f7414..c41d287868 100644
--- a/builtin/panic_nonjs_test.mbt
+++ b/builtin/panic_nonjs_test.mbt
@@ -14,6 +14,6 @@
///|
test "panic array_pop_exn_empty" {
- let arr : Array[Int] = Array::new()
+ let arr : Array[Int] = Array()
arr.unsafe_pop() |> ignore // This should panic
}
diff --git a/builtin/panic_test.mbt b/builtin/panic_test.mbt
index 90dc556dcf..d3f33c4eab 100644
--- a/builtin/panic_test.mbt
+++ b/builtin/panic_test.mbt
@@ -158,11 +158,6 @@ test "panic to_octets coverage for negative number than required" {
(-123456789N).to_octets() |> ignore
}
-///|
-test "panic from_octets coverage for empty octets" {
- BigInt::from_octets(b"") |> ignore
-}
-
///|
test "panic sub_string with invalid byte_length" {
let bytes = b"Hello, World!"
diff --git a/builtin/physical_equal_nan_test.mbt b/builtin/physical_equal_nan_test.mbt
new file mode 100644
index 0000000000..2fbbfe9d55
--- /dev/null
+++ b/builtin/physical_equal_nan_test.mbt
@@ -0,0 +1,111 @@
+// 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.
+
+// These tests *document* what `physical_equal` currently does with NaN; they do
+// not specify it. `physical_equal` is explicitly a performance hint whose result
+// "may not be consistent across different backends and/or different compiler
+// optimization settings", so nothing outside this file should depend on these
+// answers. They are pinned here so that a change in any backend shows up as a
+// visible diff rather than as a silent behavior change.
+//
+// As of this commit every case below produces the same answer on all four
+// targets (native, js, wasm, wasm-gc), so no per-target expectations are needed.
+
+///|
+/// On an unboxed float, `%refeq` lowers to an IEEE-754 value comparison, so
+/// `physical_equal` inherits the fact that NaN is not equal to itself: it is
+/// *not* reflexive on NaN, not even when both arguments are the very same
+/// binding.
+test "physical_equal is not reflexive on Double NaN" {
+ let nan = @double.not_a_number
+ inspect(physical_equal(nan, nan), content="false")
+ // For reference, the value comparison it agrees with:
+ inspect(nan == nan, content="false")
+}
+
+///|
+/// Same for `Float`.
+test "physical_equal is not reflexive on Float NaN" {
+ let nan = @float.not_a_number
+ inspect(physical_equal(nan, nan), content="false")
+ inspect(nan == nan, content="false")
+}
+
+///|
+/// NaN payloads are not distinguished either: two NaNs with *different* bit
+/// patterns compare `false`, exactly like two NaNs with the same bit pattern.
+/// So `physical_equal` gives no way to tell NaNs apart.
+test "physical_equal on NaNs with different bit patterns" {
+ let quiet_nan = @double.not_a_number
+ let payload_nan = 0x7FF8_0000_0000_0DEFUL.reinterpret_as_double()
+ let negative_nan = -@double.not_a_number
+ assert_true(payload_nan.is_nan())
+ assert_true(negative_nan.is_nan())
+ inspect(physical_equal(quiet_nan, payload_nan), content="false")
+ inspect(physical_equal(quiet_nan, negative_nan), content="false")
+ inspect(physical_equal(payload_nan, payload_nan), content="false")
+}
+
+///|
+/// NaN is the only float for which reflexivity fails. Ordinary values -- and
+/// infinities -- are physically equal whenever they are equal by value, even
+/// when they come from unrelated computations, because there is no identity to
+/// compare in the first place.
+test "physical_equal on non-NaN Doubles is value equality" {
+ let one = 1.0
+ let also_one = 0.5 + 0.5
+ inspect(physical_equal(one, also_one), content="true")
+ inspect(physical_equal(@double.infinity, @double.infinity), content="true")
+ inspect(physical_equal(0.0, -0.0), content="true")
+ inspect(0.0 == -0.0, content="true")
+}
+
+///|
+/// Reading NaN back out of a container does not change the answer: what is
+/// compared is still the loaded floating-point value.
+test "physical_equal on NaN elements of an array" {
+ let nan = @double.not_a_number
+ let arr : ReadOnlyArray[Double] = [nan, nan]
+ inspect(physical_equal(arr[0], arr[0]), content="false")
+ inspect(physical_equal(arr[1], arr[0]), content="false")
+ // The array itself is a heap value, so it *is* physically equal to itself.
+ inspect(physical_equal(arr, arr), content="true")
+}
+
+///|
+/// Boxing a NaN restores reflexivity, since the comparison is then between the
+/// boxes and never reaches the float. Two separately allocated boxes holding
+/// NaN remain distinct, as they would for any other payload.
+test "physical_equal on boxed NaN compares the box" {
+ let nan = @double.not_a_number
+ let boxed = Some(nan)
+ let other_box = Some(nan)
+ inspect(physical_equal(boxed, boxed), content="true")
+ inspect(physical_equal(boxed, other_box), content="false")
+}
+
+///|
+fn[T] physical_equal_via_generic(a : T, b : T) -> Bool {
+ physical_equal(a, b)
+}
+
+///|
+/// Going through a polymorphic call site -- where a backend might box the
+/// argument -- does not change any of the above.
+test "physical_equal on NaN through a generic call site" {
+ let nan = @double.not_a_number
+ let boxed = Some(nan)
+ inspect(physical_equal_via_generic(nan, nan), content="false")
+ inspect(physical_equal_via_generic(boxed, boxed), content="true")
+}
diff --git a/builtin/pkg.generated.mbti b/builtin/pkg.generated.mbti
index 254d09e035..ff222a7225 100644
--- a/builtin/pkg.generated.mbti
+++ b/builtin/pkg.generated.mbti
@@ -64,6 +64,7 @@ pub fn ArgsLoc::to_string(Self) -> String
pub impl Show for ArgsLoc
type Array[T]
+pub fn[T] Array::Array(capacity? : Int) -> Self[T]
pub fn[T] Array::add(Self[T], Self[T]) -> Self[T]
#alias(every)
pub fn[T] Array::all(Self[T], (T) -> Bool raise?) -> Bool raise?
@@ -128,10 +129,12 @@ pub fn[T, U] Array::mapi(Self[T], (Int, T) -> U raise?) -> Self[U] raise?
#alias(mapi_inplace, deprecated)
pub fn[T] Array::mapi_in_place(Self[T], (Int, T) -> T raise?) -> Unit raise?
pub fn[T] Array::mut_view(Self[T], start? : Int, end? : Int) -> MutArrayView[T]
+#deprecated
pub fn[T] Array::new(capacity? : Int) -> Self[T]
pub fn[T] Array::pop(Self[T]) -> T?
pub fn[T] Array::push(Self[T], T) -> Unit
pub fn[T] Array::push_iter(Self[T], Iter[T]) -> Unit
+pub fn[T] Array::release_unused(Self[T], placeholder~ : T) -> Unit
pub fn[T] Array::remove(Self[T], Int) -> T
pub fn[T] Array::repeat(Self[T], Int) -> Self[T]
pub fn[T] Array::reserve_capacity(Self[T], Int) -> Unit
@@ -343,8 +346,6 @@ pub fn[X, Y] Iter2::iter2(Self[X, Y]) -> Self[X, Y]
pub fn[X, Y] Iter2::new(() -> (X, Y)?, size_hint? : Int) -> Self[X, Y]
pub fn[X, Y] Iter2::next(Self[X, Y]) -> (X, Y)?
pub fn[X, Y] Iter2::to_array(Self[X, Y]) -> Array[(X, Y)]
-#deprecated
-pub impl[X : Show, Y : Show] Show for Iter2[X, Y]
pub enum Json {
Null
@@ -459,7 +460,7 @@ pub fn SourceLoc::to_string(Self) -> String
pub impl Show for SourceLoc
type StringBuilder
-#alias(new)
+#alias(new, deprecated)
pub fn StringBuilder::StringBuilder(size_hint? : Int) -> Self
pub fn StringBuilder::is_empty(Self) -> Bool
pub fn StringBuilder::reset(Self) -> Unit
diff --git a/builtin/range.mbt b/builtin/range.mbt
index b8e898cf44..0bbbf5a777 100644
--- a/builtin/range.mbt
+++ b/builtin/range.mbt
@@ -24,7 +24,7 @@ fn[T : Add + Compare + Default] until_impl(
) -> Iter[T] {
let zero = Default::default()
if step == zero {
- return Iter::empty()
+ return [||]
}
let mut i = start
let mut done = false
diff --git a/builtin/readonlyarray.mbt b/builtin/readonlyarray.mbt
index be660f5aa2..840ced8b80 100644
--- a/builtin/readonlyarray.mbt
+++ b/builtin/readonlyarray.mbt
@@ -61,7 +61,7 @@ pub fn[T] ReadOnlyArray::from_array(array : ArrayView[T]) -> ReadOnlyArray[T] {
/// # Example
/// ```mbt check
/// test {
-/// let iter = [1, 2, 3].iter()
+/// let iter = [|1, 2, 3|]
/// let immut_array = ReadOnlyArray::from_iter(iter)
/// inspect(immut_array[0], content="1")
/// }
@@ -100,6 +100,7 @@ pub fn[T] ReadOnlyArray::makei(
/// debug_inspect(arr.get(5), content="None")
/// }
/// ```
+#intrinsic("%readonlyarray.get_opt")
pub fn[T] ReadOnlyArray::get(self : ReadOnlyArray[T], index : Int) -> T? {
self.unsafe_reinterpret_to_fixed_array().get(index)
}
@@ -457,7 +458,7 @@ pub fn[A, B] ReadOnlyArray::foldi(
/// test {
/// let arr : ReadOnlyArray[Int] = [2, 3]
/// let sum = arr.rev_foldi(init=0, fn(i, acc, x) { acc + i * x })
-/// inspect(sum, content="2") // 0 + (1*3) + (0*2) = 3
+/// inspect(sum, content="2") // 0 + (0*3) + (1*2) = 2
/// }
/// ```
pub fn[A, B] ReadOnlyArray::rev_foldi(
@@ -866,6 +867,7 @@ pub fn[T : Compare] ReadOnlyArray::lexical_compare(
/// inspect(view[2], content="4")
/// }
/// ```
+#intrinsic("%readonlyarray.view")
#alias("_[_:_]")
#alias(sub, deprecated="Use _[_:_] instead")
pub fn[T] ReadOnlyArray::view(
diff --git a/builtin/readonlyarray_test.mbt b/builtin/readonlyarray_test.mbt
index d027153a20..933a199552 100644
--- a/builtin/readonlyarray_test.mbt
+++ b/builtin/readonlyarray_test.mbt
@@ -20,7 +20,7 @@ test "ReadOnlyArray constructors" {
inspect(arr1[2], content="3")
// Test from_iter
- let arr2 = ReadOnlyArray::from_iter([4, 5, 6].iter())
+ let arr2 = ReadOnlyArray::from_iter([|4, 5, 6|])
inspect(arr2[1], content="5")
// Test makei
diff --git a/builtin/simd.mbt b/builtin/simd.mbt
index 1bb82a4583..a3a6d59853 100644
--- a/builtin/simd.mbt
+++ b/builtin/simd.mbt
@@ -126,6 +126,13 @@ fn v128_and(a : V128, b : V128) -> V128 {
v128_make(v128_lo(a) & v128_lo(b), v128_hi(a) & v128_hi(b))
}
+///|
+#cfg(any(target="native", target="wasm"))
+#intrinsic("%v128.v128_or")
+fn v128_or(a : V128, b : V128) -> V128 {
+ v128_make(v128_lo(a) | v128_lo(b), v128_hi(a) | v128_hi(b))
+}
+
///|
#cfg(any(target="native", target="wasm"))
#intrinsic("%v128.v128_any_true")
diff --git a/builtin/string.mbt b/builtin/string.mbt
index 6b0659090a..31145b7885 100644
--- a/builtin/string.mbt
+++ b/builtin/string.mbt
@@ -55,7 +55,7 @@ fn code_point_of_surrogate_pair(leading : Int, trailing : Int) -> Char {
/// let s = "Hello🤣"
/// inspect(s.char_length(), content="6") // 6 actual characters
/// inspect(s.length(), content="7")
-/// } // 5 ASCII chars + 2 surrogate pairs
+/// } // 5 ASCII chars + 1 surrogate pair (2 code units)
/// ```
#alias(codepoint_length, deprecated)
pub fn String::char_length(
@@ -257,7 +257,7 @@ fn unsafe_to_bytes(array : FixedArray[Byte]) -> Bytes = "%identity"
pub fn String::to_array(self : String) -> Array[Char] {
self
.iter()
- .fold(init=Array::new(capacity=self.length()), (rv, c) => {
+ .fold(init=Array(capacity=self.length()), (rv, c) => {
rv.push(c)
rv
})
@@ -277,7 +277,7 @@ pub fn String::code_units(self : String) -> ArrayView[UInt16] {
/// Returns an iterator over the Unicode characters in the string.
///
/// Note: This iterator yields Unicode characters, not Utf16 code units.
-/// As a result, the count of characters returned by `iterator().count()` may not be equal to the length of the string returned by `length()`.
+/// As a result, the count of characters returned by `iter().count()` may not be equal to the length of the string returned by `length()`.
///
/// ```mbt check
/// test {
@@ -375,7 +375,7 @@ pub fn String::any(self : String, f : (Char) -> Bool raise?) -> Bool raise? {
/// - The function iterates over the string in reverse order.
/// - If a trailing surrogate is encountered, it checks for a preceding leading surrogate to form a complete Unicode code point.
/// - Yields each character or combined code point to the iterator.
-/// - Stops iteration if the `yield_` function returns `IterEnd`.
+/// - Stops once the beginning of the string is reached, after which the iterator yields `None`.
///
/// # Examples
///
diff --git a/builtin/string_char_set_bench_test.mbt b/builtin/string_char_set_bench_test.mbt
new file mode 100644
index 0000000000..93094464f5
--- /dev/null
+++ b/builtin/string_char_set_bench_test.mbt
@@ -0,0 +1,117 @@
+// 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.
+
+///|
+let string_char_set_bench_size = 100_000
+
+///|
+let string_char_set_bench_ascii_set = "z \t\n\r"
+
+///|
+let string_char_set_bench_contains_miss : String = "a".repeat(
+ string_char_set_bench_size,
+)
+
+///|
+let string_char_set_bench_contains_match_at_end : String = "a".repeat(
+ string_char_set_bench_size - 1,
+ ) +
+ "z"
+
+///|
+let string_char_set_bench_trim_start : String = " ".repeat(
+ string_char_set_bench_size,
+ ) +
+ "x"
+
+///|
+let string_char_set_bench_trim_end : String = "x" +
+ " ".repeat(string_char_set_bench_size)
+
+///|
+test "bench StringView::contains_any ASCII miss n=100000" (it : @bench.T) {
+ it.bench(fn() {
+ it.keep(
+ string_char_set_bench_contains_miss.contains_any(
+ chars=string_char_set_bench_ascii_set,
+ ),
+ )
+ })
+}
+
+///|
+test "bench StringView::contains_any ASCII match at end n=100000" (
+ it : @bench.T,
+) {
+ it.bench(fn() {
+ it.keep(
+ string_char_set_bench_contains_match_at_end.contains_any(
+ chars=string_char_set_bench_ascii_set,
+ ),
+ )
+ })
+}
+
+///|
+test "bench StringView::trim_start ASCII n=100000" (it : @bench.T) {
+ it.bench(fn() {
+ it.keep(string_char_set_bench_trim_start.trim_start().length())
+ })
+}
+
+///|
+test "bench StringView::trim_end ASCII n=100000" (it : @bench.T) {
+ it.bench(fn() { it.keep(string_char_set_bench_trim_end.trim_end().length()) })
+}
+
+///|
+test "bench StringView::trim ASCII n=100000" (it : @bench.T) {
+ let input = string_char_set_bench_trim_start +
+ " ".repeat(string_char_set_bench_size)
+ it.bench(fn() { it.keep(input.trim().length()) })
+}
+
+///|
+let string_char_set_short_trim_inputs : Array[String] = [
+ " hello ", "x", "", " ", "no_trim_needed", "\t indented line \n",
+]
+
+///|
+let string_char_set_short_contains_inputs : Array[String] = [
+ "hello,world", "a=b&c=d", "plain", "",
+]
+
+///|
+test "bench StringView::trim short inputs" (it : @bench.T) {
+ it.bench(fn() {
+ let mut total = 0
+ for s in string_char_set_short_trim_inputs {
+ total += s.trim().length()
+ }
+ it.keep(total)
+ })
+}
+
+///|
+test "bench StringView::contains_any short inputs" (it : @bench.T) {
+ it.bench(fn() {
+ let mut hits = 0
+ for s in string_char_set_short_contains_inputs {
+ if s.contains_any(chars=",&= ") {
+ hits += 1
+ }
+ }
+ it.keep(hits)
+ })
+}
diff --git a/builtin/string_char_set_quickcheck_test.mbt b/builtin/string_char_set_quickcheck_test.mbt
new file mode 100644
index 0000000000..1ed7f11aeb
--- /dev/null
+++ b/builtin/string_char_set_quickcheck_test.mbt
@@ -0,0 +1,226 @@
+// 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.
+
+// Property-based tests for the character-set paths of `contains_any` and
+// `trim`/`trim_start`/`trim_end`: the bitmap and SIMD scans must agree with a
+// straightforward character-by-character model, on strings and on views with
+// non-zero offsets, for ASCII sets of every size class (SIMD-broadcast,
+// bitmap-only, and the non-ASCII fallback).
+
+///|
+/// Characters a haystack is built from: mostly ASCII (including every member
+/// of the generated sets and both sides of the 0x20 boundary), plus non-ASCII
+/// BMP and non-BMP characters so surrogate pairs appear.
+fn haystack_char(seed : Int) -> Char {
+ match seed & 0xF {
+ 0 => ' '
+ 1 => '\t'
+ 2 => '\n'
+ 3 => '\r'
+ 4 => ','
+ 5 => ';'
+ 6 => 'z'
+ 7 => 'a'
+ 8 => '\u{00}'
+ 9 => '\u{1F}'
+ 10 => '\u{7F}'
+ 11 => '中'
+ 12 => '😀'
+ _ => (0x20 + ((seed >> 4) & 0x3F)).unsafe_to_char()
+ }
+}
+
+///|
+/// Set members: mostly ASCII delimiters; occasionally non-ASCII, which sends
+/// the whole set down the fallback path.
+fn set_char(seed : Int) -> Char {
+ match seed & 0xF {
+ 0 => ' '
+ 1 => '\t'
+ 2 => '\n'
+ 3 => '\r'
+ 4 => ','
+ 5 => ';'
+ 6 => '/'
+ 7 => 'z'
+ 8 => '0'
+ 9 => '\u{00}'
+ 10 => '\u{7F}'
+ 11 => '中'
+ 12 => '😀'
+ _ => 'a'
+ }
+}
+
+///|
+fn chars_to_string(chars : Array[Char]) -> String {
+ let buf = StringBuilder(size_hint=chars.length())
+ for c in chars {
+ buf.write_char(c)
+ }
+ buf.to_string()
+}
+
+///|
+fn model_member(chars : String, c : Char) -> Bool {
+ for d in chars {
+ if c == d {
+ return true
+ }
+ }
+ false
+}
+
+///|
+fn model_contains_any(s : String, chars : String) -> Bool {
+ for c in s {
+ if model_member(chars, c) {
+ return true
+ }
+ }
+ false
+}
+
+///|
+fn model_trim_start(s : String, chars : String) -> String {
+ let arr = s.to_array()
+ let mut start = 0
+ while start < arr.length() && model_member(chars, arr[start]) {
+ start += 1
+ }
+ chars_to_string(arr[start:].to_owned())
+}
+
+///|
+fn model_trim_end(s : String, chars : String) -> String {
+ let arr = s.to_array()
+ let mut end = arr.length()
+ while end > 0 && model_member(chars, arr[end - 1]) {
+ end -= 1
+ }
+ chars_to_string(arr[:end].to_owned())
+}
+
+///|
+/// Embeds `middle` between `prefix`/`suffix` and returns the view covering
+/// exactly `middle`, so the scans see non-zero view offsets.
+fn embedded_view(
+ prefix : String,
+ middle : String,
+ suffix : String,
+) -> StringView {
+ let full = prefix + middle + suffix
+ full[prefix.length():prefix.length() + middle.length()]
+}
+
+///|
+test "quickcheck: contains_any agrees with the character model" {
+ @quickcheck.check(count=300, (input : (Array[Int], Array[Int])) => {
+ let (hay_seeds, set_seeds) = input
+ let hay = chars_to_string(hay_seeds.map(haystack_char))
+ let set = chars_to_string(set_seeds.map(set_char))
+ // The set is passed as a view with non-zero offsets, so a scan that read
+ // beyond the set view would see the sentinel characters instead.
+ let set_view = embedded_view("Q", set, "Q")
+ let expected = model_contains_any(hay, set)
+ guard hay.contains_any(chars=set_view) == expected else { return false }
+ // The same scan through a haystack view with non-zero offsets.
+ embedded_view("ab", hay, "yz").contains_any(chars=set_view) == expected
+ })
+}
+
+///|
+test "quickcheck: trims agree with the character model" {
+ @quickcheck.check(count=300, (input : (Array[Int], Array[Int])) => {
+ let (hay_seeds, set_seeds) = input
+ let hay = chars_to_string(hay_seeds.map(haystack_char))
+ let set = chars_to_string(set_seeds.map(set_char))
+ let view = embedded_view(" 😀", hay, "😀 ")
+ let set_view = embedded_view("Q", set, "Q")
+ guard view.trim_start(chars=set_view).to_owned() ==
+ model_trim_start(hay, set) else {
+ return false
+ }
+ guard view.trim_end(chars=set_view).to_owned() == model_trim_end(hay, set) else {
+ return false
+ }
+ // The fused trim must agree with composing the two one-sided trims.
+ view.trim(chars=set_view).to_owned() ==
+ model_trim_end(model_trim_start(hay, set), set)
+ })
+}
+
+///|
+/// Exhaustively places a set member at every position of an otherwise clean
+/// string for every length spanning several 8-unit SIMD blocks, for a
+/// SIMD-broadcast-sized set, a bitmap-only-sized set, and the default
+/// whitespace set.
+test "char set boundary sweep" {
+ let sets = [" \t\n\r", ",;:.!?", "abcdefghij"]
+ for set in sets {
+ let probe = set[0:1].to_owned()
+ for len in 0..<=24 {
+ let clean = "x".repeat(len)
+ assert_false(clean.contains_any(chars=set))
+ assert_eq(clean.trim(chars=set).to_owned(), clean)
+ for pos in 0.. String {
+ String::make(half, 'a') + "Z" + String::make(half - 1, 'a')
+}
+
+///|
+test "bench find adversary m=64 n=4096" (it : @bench.T) {
+ let haystack = String::make(4096, 'a')
+ let needle = string_find_adversary_needle(32)
+ it.bench(fn() { it.keep(haystack.find(needle)) })
+}
+
+///|
+test "bench rev_find adversary m=64 n=4096" (it : @bench.T) {
+ let haystack = String::make(4096, 'a')
+ let needle = string_find_adversary_needle(32)
+ it.bench(fn() { it.keep(haystack.rev_find(needle)) })
+}
+
+///|
+test "bench find adversary m=512 n=65536" (it : @bench.T) {
+ let haystack = String::make(65536, 'a')
+ let needle = string_find_adversary_needle(256)
+ it.bench(fn() { it.keep(haystack.find(needle)) })
+}
+
+///|
+test "bench find dense miss fast path n=4096" (it : @bench.T) {
+ let haystack = String::make(4096, 'a')
+ it.bench(fn() { it.keep(haystack.find("aaaaZ")) })
+}
+
+///|
+test "bench find rare hit end fast path n=4096" (it : @bench.T) {
+ let haystack = String::make(4092, 'a') + "Zabc"
+ it.bench(fn() { it.keep(haystack.find("Zabc")) })
+}
diff --git a/builtin/string_find_code_unit.mbt b/builtin/string_find_code_unit.mbt
index a66252a5b2..e48515ddab 100644
--- a/builtin/string_find_code_unit.mbt
+++ b/builtin/string_find_code_unit.mbt
@@ -211,7 +211,7 @@ fn find_by_two_anchors(target : StringView, pattern : StringView) -> Int? {
let failures = failures + 1
let scanned = found - target_start
if two_anchor_should_fallback(failures, scanned) {
- break find_pattern_scalar_from(target, pattern, scanned + 1)
+ break find_pattern_kmp_from(target, pattern, scanned + 1)
}
continue found + 1, failures
} nobreak {
@@ -259,7 +259,7 @@ fn rev_find_by_two_anchors(target : StringView, pattern : StringView) -> Int? {
let failures = failures + 1
let scanned = last_candidate - candidate
if two_anchor_should_fallback(failures, scanned) {
- break rev_find_pattern_scalar_before(target, pattern, candidate)
+ break rev_find_pattern_kmp_before(target, pattern, candidate)
}
continue found, failures
} nobreak {
@@ -269,80 +269,133 @@ fn rev_find_by_two_anchors(target : StringView, pattern : StringView) -> Int? {
///|
// Dense first/last-anchor matches make repeated SIMD candidate scans more
-// expensive than direct comparison. Cut over after enough false candidates,
-// following the guarded-scanner strategy used by Bytes search.
+// expensive than direct comparison. Cut over to the guaranteed-linear KMP
+// fallback either early, when failures pile up relative to progress
+// (`failures > 4 + scanned / 8` — as soon as the 5th failure for
+// candidates packed near the scan start), or at the hard cap
+// (`failures > 64`, i.e. the 65th failed verification). Either way the
+// failure count at cutover is at most 65, keeping pre-cutover
+// verification work at O(pattern), so find/rev_find stay
+// O(target + pattern) even on adversarial inputs.
#inline
fn two_anchor_should_fallback(failures : Int, scanned : Int) -> Bool {
failures > 64 || failures > 4 + scanned / 8
}
///|
-// Direct forward fallback used after the two-anchor filter encounters dense
-// false positives. It checks the middle first because the cutover has already
-// established that first/last matches are not selective. `start` is relative
-// to `target`.
-fn find_pattern_scalar_from(
+// Longest-proper-border table for the KMP fallbacks: `table[i]` is the
+// length of the longest proper prefix of `pattern[0..=i]` that is also a
+// suffix of it. Only built after the two-anchor filter cuts over, so the
+// O(pattern) allocation is paid exclusively on pathological inputs.
+fn kmp_failure_table(pattern : StringView) -> FixedArray[Int] {
+ let m = pattern.length()
+ let table = FixedArray::make(m, 0)
+ let mut k = 0
+ for i in 1.. 0 && c != pattern.unsafe_get(k) {
+ k = table[k - 1]
+ }
+ if c == pattern.unsafe_get(k) {
+ k += 1
+ }
+ table[i] = k
+ }
+ table
+}
+
+///|
+// Guaranteed-linear forward fallback used after the two-anchor filter
+// encounters dense false positives: KMP over the remaining candidates, so
+// the whole search stays O(target + pattern) even when both anchors and
+// long pattern prefixes recur throughout the target. Returns the first
+// occurrence starting at a target-relative position >= `start`.
+fn find_pattern_kmp_from(
target : StringView,
pattern : StringView,
start : Int,
) -> Int? {
- let pattern_len = pattern.length()
- let last_offset = pattern_len - 1
- let first = pattern.unsafe_get(0)
- let last = pattern.unsafe_get(last_offset)
- let last_candidate = target.length() - pattern_len
- for candidate in start..<=last_candidate {
- let middle_matches = for i in 1.. 0 && c != pattern.unsafe_get(k) {
+ k = table[k - 1]
}
- if middle_matches &&
- target.unsafe_get(candidate) == first &&
- target.unsafe_get(candidate + last_offset) == last {
- break Some(candidate)
+ if c == pattern.unsafe_get(k) {
+ k += 1
+ }
+ if k == m {
+ return Some(i - m + 1)
}
- } nobreak {
- None
}
+ None
}
///|
-// Direct reverse fallback used after the two-anchor filter encounters dense
-// false positives. It checks the middle first because the cutover has already
-// established that first/last matches are not selective. `candidate_end` is
-// target-relative and exclusive.
-fn rev_find_pattern_scalar_before(
+// Guaranteed-linear reverse fallback used after the two-anchor filter
+// encounters dense false positives: forward KMP over the prefix that can
+// still contain a hit, keeping the rightmost match, so the whole search
+// stays O(target + pattern). Returns the last occurrence starting at a
+// target-relative position strictly below `candidate_end` (exclusive).
+// The caller must ensure
+// `candidate_end <= target.length() - pattern.length() + 1`, so that the
+// scan below stays in bounds.
+fn rev_find_pattern_kmp_before(
target : StringView,
pattern : StringView,
candidate_end : Int,
) -> Int? {
guard candidate_end > 0 else { return None }
- let pattern_len = pattern.length()
- let last_offset = pattern_len - 1
- let first = pattern.unsafe_get(0)
- let last = pattern.unsafe_get(last_offset)
- for candidate = candidate_end - 1; candidate >= 0; {
- let middle_matches = for i in 1.. 0 && c != pattern.unsafe_get(k) {
+ k = table[k - 1]
}
- if middle_matches &&
- target.unsafe_get(candidate) == first &&
- target.unsafe_get(candidate + last_offset) == last {
- break Some(candidate)
+ if c == pattern.unsafe_get(k) {
+ k += 1
}
- continue candidate - 1
- } nobreak {
+ if k == m {
+ best = i - m + 1
+ // keep scanning: a later (more rightward) overlapping match wins
+ k = table[k - 1]
+ }
+ }
+ if best >= 0 {
+ Some(best)
+ } else {
None
}
}
+///|
+test "kmp fallbacks handle periodic and overlapping patterns" {
+ // failure table borders matter for periodic patterns
+ let t : StringView = "aaaaaa"
+ assert_true(find_pattern_kmp_from(t, "aaa", 0) is Some(0))
+ assert_true(find_pattern_kmp_from(t, "aaa", 2) is Some(2))
+ assert_true(find_pattern_kmp_from(t, "aaa", 4) is None)
+ assert_true(rev_find_pattern_kmp_before(t, "aaa", 4) is Some(3))
+ assert_true(rev_find_pattern_kmp_before(t, "aaa", 1) is Some(0))
+ let u : StringView = "ababcababab"
+ assert_true(find_pattern_kmp_from(u, "abab", 0) is Some(0))
+ assert_true(find_pattern_kmp_from(u, "abab", 1) is Some(5))
+ assert_true(rev_find_pattern_kmp_before(u, "abab", 8) is Some(7))
+ assert_true(rev_find_pattern_kmp_before(u, "ababc", 7) is Some(0))
+ assert_true(find_pattern_kmp_from(u, "abcabc", 0) is None)
+ // start beyond any match and empty prefix bound
+ assert_true(rev_find_pattern_kmp_before(u, "abab", 0) is None)
+}
+
///|
// Compares two raw UTF-16 ranges. The caller must ensure both ranges are in
// bounds; using backing strings directly also permits isolated surrogate code
diff --git a/builtin/string_find_fallback_test.mbt b/builtin/string_find_fallback_test.mbt
new file mode 100644
index 0000000000..cd879021de
--- /dev/null
+++ b/builtin/string_find_fallback_test.mbt
@@ -0,0 +1,161 @@
+// 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.
+
+// End-to-end coverage for the guaranteed-linear KMP fallback behind the
+// SIMD two-anchor substring search. The cutover has two triggers: an
+// early ratio check (`failures > 4 + scanned / 8`) and a hard cap
+// (`failures > 64`). The decoy constructions here space their false
+// candidates one period apart, which keeps the ratio check quiet, so
+// these tests deterministically force the HARD-CAP path by providing
+// more than 65 positions where both anchor code units match but
+// verification fails, in the direction under test, before the
+// interesting region — misses, hits beyond the cutover point,
+// overlapping hits resolved through the fallback, and naive-reference
+// cross-checks.
+
+///|
+fn adversarial_needle() -> String {
+ // both anchors are 'a' and a 32-unit prefix of the needle recurs at every
+ // position of an all-'a' haystack: every candidate passes the anchor test
+ // and fails only deep inside verification
+ String::make(32, 'a') + "Z" + String::make(31, 'a')
+}
+
+///|
+test "dense-anchor miss stays correct through the kmp fallback" {
+ let haystack = String::make(4096, 'a')
+ debug_inspect(haystack.find(adversarial_needle()), content="None")
+ debug_inspect(haystack.rev_find(adversarial_needle()), content="None")
+}
+
+///|
+test "hit located after the cutover point is found" {
+ let needle = adversarial_needle()
+ let haystack = String::make(2000, 'a') + needle + String::make(50, 'a')
+ debug_inspect(haystack.find(needle), content="Some(2000)")
+ debug_inspect(haystack.rev_find(needle), content="Some(2000)")
+ // a second occurrence: find returns the first, rev_find the last
+ let doubled = String::make(1000, 'a') +
+ needle +
+ String::make(1000, 'a') +
+ needle
+ debug_inspect(doubled.find(needle), content="Some(1000)")
+ debug_inspect(doubled.rev_find(needle), content="Some(2064)")
+}
+
+///|
+// One decoy block per repetition: for the periodic needle used below
+// (anchors 'a' and 'b', length 82), each block contributes exactly one
+// position whose anchors match but whose interior verification fails.
+fn decoy_blocks(count : Int) -> String {
+ (String::make(81, 'a') + "b").repeat(count)
+}
+
+///|
+// 40a b 40a b 40a b: contains the 82-unit needle 40a b 40a b at
+// offsets 0 and 41 — two overlapping occurrences.
+fn overlapping_hits_block() -> String {
+ let half = String::make(40, 'a') + "b"
+ half + half + half
+}
+
+///|
+test "forward cutover then kmp finds the first hit" {
+ let needle = String::make(40, 'a') + "b" + String::make(40, 'a') + "b"
+ // 70 false candidates precede the hits, exhausting the 65-failure budget;
+ // the first genuine occurrence straddles the last decoy block and the
+ // hits block (40 decoy 'a's, the decoy 'b' at 5739, 40 hit 'a's, and the
+ // hit 'b' at 5780), so kmp must report 5699, not the hits-block start
+ let haystack = decoy_blocks(70) + overlapping_hits_block()
+ debug_inspect(haystack.find(needle), content="Some(5699)")
+}
+
+///|
+test "reverse cutover then kmp keeps the rightmost overlapping hit" {
+ let needle = String::make(40, 'a') + "b" + String::make(40, 'a') + "b"
+ // hits sit at the start; 70 false candidates follow them, so the reverse
+ // scan burns its budget before reaching the hits and hands off to the
+ // kmp fallback, which must resolve the overlap to the rightmost start
+ let haystack = overlapping_hits_block() + decoy_blocks(70)
+ debug_inspect(haystack.rev_find(needle), content="Some(41)")
+ // sanity: the forward search over the same input needs no fallback
+ debug_inspect(haystack.find(needle), content="Some(0)")
+}
+
+///|
+fn naive_find(haystack : String, needle : String) -> Int? {
+ let n = haystack.length()
+ let m = needle.length()
+ for start in 0..<=(n - m) {
+ let matched = for i in 0.. Int? {
+ let n = haystack.length()
+ let m = needle.length()
+ for start = n - m; start >= 0; {
+ let matched = for i in 0.. String {
+ let sb = StringBuilder(size_hint=bits.length())
+ for b in bits {
+ sb.write_char(if b { 'a' } else { 'b' })
+ }
+ sb.to_string()
+}
+
+///|
+test "quickcheck: find and rev_find agree with naive references" {
+ @quickcheck.check(
+ (input : (Array[Bool], Array[Bool])) => {
+ let (text_bits, needle_bits) = input
+ let text = bools_to_ab(text_bits)
+ // clamp the needle into the multi-unit regime the two-anchor search
+ // handles; single-unit and empty needles take separate code paths
+ guard needle_bits.length() >= 2 else { return true }
+ let needle = bools_to_ab(needle_bits)
+ text.find(needle) == naive_find(text, needle) &&
+ text.rev_find(needle) == naive_rev_find(text, needle)
+ },
+ count=200,
+ )
+}
+
+///|
+test "quickcheck: fallback-forcing inputs agree with naive references" {
+ @quickcheck.check(
+ (input : (UInt, UInt, UInt, Array[Bool])) => {
+ let (p0, q0, flip0, mix) = input
+ // needle 'a'*p + 'b' + 'a'*q: anchors are 'a'/'a' (or 'a'/'b' when
+ // q == 0), interior mostly 'a' — the adversarial family
+ let p = (p0 % 14).reinterpret_as_int() + 1
+ let q = (q0 % 15).reinterpret_as_int()
+ let needle = String::make(p, 'a') + "b" + String::make(q, 'a')
+ let m = needle.length()
+ guard m >= 3 else { return true }
+ // corrupted block: one interior char flipped, anchors intact, so a
+ // block-start candidate passes the anchor test and fails verification
+ let flip = 1 +
+ (flip0 % (m - 2).reinterpret_as_uint()).reinterpret_as_int()
+ let sb = StringBuilder(size_hint=m)
+ for i in 0.. Int? {
find_by_two_anchors(self, str)
}
}
- // TODO: When the pattern string is long (>= 256),
- // consider using Two-Way algorithm to ensure linear time complexity.
+ // Worst-case linearity: after dense false-anchor candidates the search
+ // cuts over to a KMP fallback (see the two-anchor search internals), so
+ // the total cost stays O(self + str) even on adversarial inputs.
}
///|
@@ -145,8 +146,9 @@ pub fn StringView::rev_find(self : StringView, str : StringView) -> Int? {
rev_find_by_two_anchors(self, str)
}
}
- // TODO: When the pattern string is long (>= 256),
- // consider using Two-Way algorithm to ensure linear time complexity.
+ // Worst-case linearity: after dense false-anchor candidates the search
+ // cuts over to a KMP fallback (see the two-anchor search internals), so
+ // the total cost stays O(self + str) even on adversarial inputs.
}
///|
@@ -420,7 +422,7 @@ pub fn StringView::strip_suffix(
pub fn StringView::to_array(self : StringView) -> Array[Char] {
self
.iter()
- .fold(init=Array::new(capacity=self.length()), (rv, c) => {
+ .fold(init=Array(capacity=self.length()), (rv, c) => {
rv.push(c)
rv
})
@@ -566,6 +568,433 @@ pub fn String::contains_code_unit(self : String, code : UInt16) -> Bool {
string_contains_code_unit(self, 0, self.length(), code)
}
+///|
+const ASCII_CHAR_SET_LIMIT : UInt = 128U
+
+///|
+const ASCII_CHAR_SET_WORD_MASK : UInt = 31U
+
+///|
+const ASCII_CHAR_SET_WORD_SHIFT = 5
+
+///|
+// At most this many set members are broadcast to vectors by the SIMD scan;
+// larger sets use the scalar bitmap scan.
+#cfg(any(target="native", target="wasm"))
+const ASCII_CHAR_SET_SIMD_MAX_CHARS = 8
+
+///|
+#inline
+fn build_ascii_char_set(chars : StringView) -> (UInt, UInt, UInt, UInt)? {
+ let mut bits0 = 0U
+ let mut bits1 = 0U
+ let mut bits2 = 0U
+ let mut bits3 = 0U
+ for c in chars {
+ let code = c.to_uint()
+ guard code < ASCII_CHAR_SET_LIMIT else { return None }
+ let bit = 1U << (code & ASCII_CHAR_SET_WORD_MASK).reinterpret_as_int()
+ match code >> ASCII_CHAR_SET_WORD_SHIFT {
+ 0 => bits0 = bits0 | bit
+ 1 => bits1 = bits1 | bit
+ 2 => bits2 = bits2 | bit
+ _ => bits3 = bits3 | bit
+ }
+ }
+ Some((bits0, bits1, bits2, bits3))
+}
+
+///|
+/// Tests membership in a 128-bit ASCII character set represented by four
+/// scalar words, so callers do not need a temporary heap allocation. Code
+/// units outside the ASCII range are never members.
+///
+/// An ASCII code unit is never half of a surrogate pair, so for ASCII-only
+/// sets scanning raw code units is equivalent to scanning characters.
+#inline
+fn ascii_char_set_contains(
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+ code : UInt,
+) -> Bool {
+ guard code < ASCII_CHAR_SET_LIMIT else { return false }
+ let bit = 1U << (code & ASCII_CHAR_SET_WORD_MASK).reinterpret_as_int()
+ match code >> ASCII_CHAR_SET_WORD_SHIFT {
+ 0 => (bits0 & bit) != 0U
+ 1 => (bits1 & bit) != 0U
+ 2 => (bits2 & bit) != 0U
+ _ => (bits3 & bit) != 0U
+ }
+}
+
+///|
+// The caller must ensure `0 <= start <= end <= str.length()`.
+#cfg(not(target="js"))
+#inline
+fn string_contains_any_ascii_scalar(
+ str : String,
+ start : Int,
+ end : Int,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Bool {
+ for i in start.. Int {
+ for pos = start {
+ if pos < end &&
+ ascii_char_set_contains(
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ str.unsafe_get(pos).to_uint(),
+ ) {
+ continue pos + 1
+ } else {
+ break pos
+ }
+ }
+}
+
+///|
+// Returns the position just past the last code unit in `start.. Int {
+ for pos = end {
+ if pos > start &&
+ ascii_char_set_contains(
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ str.unsafe_get(pos - 1).to_uint(),
+ ) {
+ continue pos - 1
+ } else {
+ break pos
+ }
+ }
+}
+
+///|
+// Broadcasts the set member at `index` (repeating the first member for unused
+// slots, so the compare tree stays branchless) for the SIMD scan.
+#cfg(any(target="native", target="wasm"))
+#inline
+fn ascii_char_set_splat(chars : StringView, count : Int, index : Int) -> V128 {
+ i16x8_splat(chars.unsafe_get(if index < count { index } else { 0 }))
+}
+
+///|
+// Per-lane mask of which of the eight code units in `block` are members of
+// the set broadcast across `s0..s7`.
+#cfg(any(target="native", target="wasm"))
+#inline
+fn ascii_char_set_block_mask(
+ block : V128,
+ s0 : V128,
+ s1 : V128,
+ s2 : V128,
+ s3 : V128,
+ s4 : V128,
+ s5 : V128,
+ s6 : V128,
+ s7 : V128,
+) -> V128 {
+ v128_or(
+ v128_or(
+ v128_or(i16x8_eq(block, s0), i16x8_eq(block, s1)),
+ v128_or(i16x8_eq(block, s2), i16x8_eq(block, s3)),
+ ),
+ v128_or(
+ v128_or(i16x8_eq(block, s4), i16x8_eq(block, s5)),
+ v128_or(i16x8_eq(block, s6), i16x8_eq(block, s7)),
+ ),
+ )
+}
+
+///|
+// On the JavaScript backend the character iterator compiles to a faster loop
+// than indexed code-unit reads, and for an ASCII set the two scans agree.
+#cfg(target="js")
+fn string_contains_any_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ _chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Bool {
+ StringView::make_view(str, start, end).contains_any_ascii_chars(
+ bits0, bits1, bits2, bits3,
+ )
+}
+
+///|
+#cfg(target="js")
+fn StringView::contains_any_ascii_chars(
+ self : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Bool {
+ for c in self {
+ if ascii_char_set_contains(bits0, bits1, bits2, bits3, c.to_uint()) {
+ return true
+ }
+ }
+ false
+}
+
+///|
+#cfg(not(any(target="native", target="wasm", target="js")))
+fn string_contains_any_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ _chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Bool {
+ string_contains_any_ascii_scalar(str, start, end, bits0, bits1, bits2, bits3)
+}
+
+///|
+// Scan eight UTF-16 code units at a time on linear-memory backends, comparing
+// each block against every set member at once, then scan the remaining tail
+// one code unit at a time.
+#cfg(any(target="native", target="wasm"))
+fn string_contains_any_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Bool {
+ let count = chars.length()
+ guard count >= 1 && count <= ASCII_CHAR_SET_SIMD_MAX_CHARS && start + 8 <= end else {
+ return string_contains_any_ascii_scalar(
+ str, start, end, bits0, bits1, bits2, bits3,
+ )
+ }
+ let s0 = ascii_char_set_splat(chars, count, 0)
+ let s1 = ascii_char_set_splat(chars, count, 1)
+ let s2 = ascii_char_set_splat(chars, count, 2)
+ let s3 = ascii_char_set_splat(chars, count, 3)
+ let s4 = ascii_char_set_splat(chars, count, 4)
+ let s5 = ascii_char_set_splat(chars, count, 5)
+ let s6 = ascii_char_set_splat(chars, count, 6)
+ let s7 = ascii_char_set_splat(chars, count, 7)
+ let tail_start = for pos = start; pos + 8 <= end; {
+ let block = v128_load_i16x8(str, pos)
+ if v128_any_true(
+ ascii_char_set_block_mask(block, s0, s1, s2, s3, s4, s5, s6, s7),
+ ) {
+ return true
+ }
+ continue pos + 8
+ } nobreak {
+ pos
+ }
+ string_contains_any_ascii_scalar(
+ str, tail_start, end, bits0, bits1, bits2, bits3,
+ )
+}
+
+///|
+#cfg(not(any(target="native", target="wasm")))
+fn string_trim_start_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ _chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Int {
+ string_trim_start_ascii_scalar(str, start, end, bits0, bits1, bits2, bits3)
+}
+
+///|
+// Skip eight fully-trimmable code units at a time on linear-memory backends;
+// the first block containing a non-member (and the sub-8 tail) is finished by
+// the scalar scan.
+#cfg(any(target="native", target="wasm"))
+fn string_trim_start_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Int {
+ let count = chars.length()
+ guard count >= 1 && count <= ASCII_CHAR_SET_SIMD_MAX_CHARS && start + 8 <= end else {
+ return string_trim_start_ascii_scalar(
+ str, start, end, bits0, bits1, bits2, bits3,
+ )
+ }
+ let s0 = ascii_char_set_splat(chars, count, 0)
+ let s1 = ascii_char_set_splat(chars, count, 1)
+ let s2 = ascii_char_set_splat(chars, count, 2)
+ let s3 = ascii_char_set_splat(chars, count, 3)
+ let s4 = ascii_char_set_splat(chars, count, 4)
+ let s5 = ascii_char_set_splat(chars, count, 5)
+ let s6 = ascii_char_set_splat(chars, count, 6)
+ let s7 = ascii_char_set_splat(chars, count, 7)
+ let boundary = for pos = start; pos + 8 <= end; {
+ let block = v128_load_i16x8(str, pos)
+ let mask = ascii_char_set_block_mask(block, s0, s1, s2, s3, s4, s5, s6, s7)
+ if i16x8_bitmask(mask) != 0xFF {
+ break pos
+ }
+ continue pos + 8
+ } nobreak {
+ pos
+ }
+ string_trim_start_ascii_scalar(str, boundary, end, bits0, bits1, bits2, bits3)
+}
+
+///|
+#cfg(not(any(target="native", target="wasm")))
+fn string_trim_end_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ _chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Int {
+ string_trim_end_ascii_scalar(str, start, end, bits0, bits1, bits2, bits3)
+}
+
+///|
+// Mirror of `string_trim_start_ascii`, scanning blocks backward from the end.
+#cfg(any(target="native", target="wasm"))
+fn string_trim_end_ascii(
+ str : String,
+ start : Int,
+ end : Int,
+ chars : StringView,
+ bits0 : UInt,
+ bits1 : UInt,
+ bits2 : UInt,
+ bits3 : UInt,
+) -> Int {
+ let count = chars.length()
+ guard count >= 1 && count <= ASCII_CHAR_SET_SIMD_MAX_CHARS && start + 8 <= end else {
+ return string_trim_end_ascii_scalar(
+ str, start, end, bits0, bits1, bits2, bits3,
+ )
+ }
+ let s0 = ascii_char_set_splat(chars, count, 0)
+ let s1 = ascii_char_set_splat(chars, count, 1)
+ let s2 = ascii_char_set_splat(chars, count, 2)
+ let s3 = ascii_char_set_splat(chars, count, 3)
+ let s4 = ascii_char_set_splat(chars, count, 4)
+ let s5 = ascii_char_set_splat(chars, count, 5)
+ let s6 = ascii_char_set_splat(chars, count, 6)
+ let s7 = ascii_char_set_splat(chars, count, 7)
+ let boundary = for pos = end; pos - 8 >= start; {
+ let block = v128_load_i16x8(str, pos - 8)
+ let mask = ascii_char_set_block_mask(block, s0, s1, s2, s3, s4, s5, s6, s7)
+ if i16x8_bitmask(mask) != 0xFF {
+ break pos
+ }
+ continue pos - 8
+ } nobreak {
+ pos
+ }
+ string_trim_end_ascii_scalar(str, start, boundary, bits0, bits1, bits2, bits3)
+}
+
+///|
+fn StringView::trim_start_with_chars(
+ self : StringView,
+ chars : StringView,
+) -> StringView {
+ for x = self {
+ match x {
+ [] as v => break v
+ [c, .. rest] as v =>
+ if chars.contains_char(c) {
+ continue rest
+ } else {
+ break v
+ }
+ }
+ }
+}
+
+///|
+fn StringView::trim_end_with_chars(
+ self : StringView,
+ chars : StringView,
+) -> StringView {
+ for x = self {
+ match x {
+ [] as v => break v
+ [.. rest, c] as v =>
+ if chars.contains_char(c) {
+ continue rest
+ } else {
+ break v
+ }
+ }
+ }
+}
+
///|
/// Returns true if this string contains any character from the given set.
pub fn StringView::contains_any(self : StringView, chars~ : StringView) -> Bool {
@@ -573,12 +1002,26 @@ pub fn StringView::contains_any(self : StringView, chars~ : StringView) -> Bool
[] => false
[c] => self.contains_char(c) // specialize for single character
_ =>
- for c in self {
- if chars.contains_char(c) {
- break true
- }
- } nobreak {
- false
+ match build_ascii_char_set(chars) {
+ Some((bits0, bits1, bits2, bits3)) =>
+ string_contains_any_ascii(
+ self.str(),
+ self.start(),
+ self.end(),
+ chars,
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ )
+ None =>
+ for c in self {
+ if chars.contains_char(c) {
+ break true
+ }
+ } nobreak {
+ false
+ }
}
}
}
@@ -647,6 +1090,37 @@ test "contains_any" {
inspect("hello"[:].contains_any(chars="eo"), content="true")
}
+///|
+test "contains_any and trim ASCII character sets" {
+ assert_true("😀a".contains_any(chars="az"))
+ assert_false("😀".contains_any(chars="az"))
+ assert_true("😀".contains_any(chars="a😀"))
+ let view = "x hello \ty"[1:10]
+ assert_true(view.trim(chars=" \t") == "hello")
+}
+
+///|
+test "build ASCII character set" {
+ match build_ascii_char_set("a z") {
+ Some((bits0, bits1, bits2, bits3)) => {
+ assert_true(
+ ascii_char_set_contains(bits0, bits1, bits2, bits3, 'a'.to_uint()),
+ )
+ assert_true(
+ ascii_char_set_contains(bits0, bits1, bits2, bits3, 'z'.to_uint()),
+ )
+ assert_true(
+ ascii_char_set_contains(bits0, bits1, bits2, bits3, ' '.to_uint()),
+ )
+ assert_false(
+ ascii_char_set_contains(bits0, bits1, bits2, bits3, 'b'.to_uint()),
+ )
+ }
+ None => assert_false(true)
+ }
+ assert_true(build_ascii_char_set("a😀") is None)
+}
+
///|
/// Returns true if this string contains the given character.
pub fn StringView::contains_char(self : StringView, c : Char) -> Bool {
@@ -718,16 +1192,21 @@ pub fn StringView::trim_start(
self : StringView,
chars? : StringView = "\t\n\r ",
) -> StringView {
- for x = self {
- match x {
- [] as v => break v
- [c, .. rest] as v =>
- if chars.contains_char(c) {
- continue rest
- } else {
- break v
- }
+ match build_ascii_char_set(chars) {
+ Some((bits0, bits1, bits2, bits3)) => {
+ let start = string_trim_start_ascii(
+ self.str(),
+ self.start(),
+ self.end(),
+ chars,
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ )
+ StringView::make_view(self.str(), start, self.end())
}
+ None => self.trim_start_with_chars(chars)
}
}
@@ -766,16 +1245,21 @@ pub fn StringView::trim_end(
self : StringView,
chars? : StringView = "\t\n\r ",
) -> StringView {
- for x = self {
- match x {
- [] as v => break v
- [.. rest, c] as v =>
- if chars.contains_char(c) {
- continue rest
- } else {
- break v
- }
+ match build_ascii_char_set(chars) {
+ Some((bits0, bits1, bits2, bits3)) => {
+ let end = string_trim_end_ascii(
+ self.str(),
+ self.start(),
+ self.end(),
+ chars,
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ )
+ StringView::make_view(self.str(), self.start(), end)
}
+ None => self.trim_end_with_chars(chars)
}
}
@@ -815,7 +1299,32 @@ pub fn StringView::trim(
self : StringView,
chars? : StringView = "\t\n\r ",
) -> StringView {
- self.trim_start(chars~).trim_end(chars~)
+ match build_ascii_char_set(chars) {
+ Some((bits0, bits1, bits2, bits3)) => {
+ let start = string_trim_start_ascii(
+ self.str(),
+ self.start(),
+ self.end(),
+ chars,
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ )
+ let end = string_trim_end_ascii(
+ self.str(),
+ start,
+ self.end(),
+ chars,
+ bits0,
+ bits1,
+ bits2,
+ bits3,
+ )
+ StringView::make_view(self.str(), start, end)
+ }
+ None => self.trim_start_with_chars(chars).trim_end_with_chars(chars)
+ }
}
///|
@@ -939,8 +1448,12 @@ test "is_blank" {
///|
/// Returns a new string with `padding_char`s prefixed to `self` if
-/// `self.char_length() < total_width`. The number of unicode characters in
-/// the returned string is `total_width` if padding is added.
+/// `self.length() < total_width`. The threshold and the pad count are
+/// measured in UTF-16 code units: `total_width - self.length()` copies of
+/// `padding_char` are prefixed. Characters outside the BMP count as two
+/// code units, so with such characters in `self` the result has fewer than
+/// `total_width` characters, and with a non-BMP `padding_char` the result's
+/// UTF-16 length exceeds `total_width`.
pub fn StringView::pad_start(
self : StringView,
total_width : Int,
@@ -954,8 +1467,12 @@ pub fn StringView::pad_start(
///|
/// Returns a new string with `padding_char`s prefixed to `self` if
-/// `self.char_length() < total_width`. The number of unicode characters in
-/// the returned string is `total_width` if padding is added.
+/// `self.length() < total_width`. The threshold and the pad count are
+/// measured in UTF-16 code units: `total_width - self.length()` copies of
+/// `padding_char` are prefixed. Characters outside the BMP count as two
+/// code units, so with such characters in `self` the result has fewer than
+/// `total_width` characters, and with a non-BMP `padding_char` the result's
+/// UTF-16 length exceeds `total_width`.
pub fn String::pad_start(
self : String,
total_width : Int,
@@ -994,8 +1511,12 @@ test "pad_start" {
///|
/// Returns a new string with `padding_char`s appended to `self` if
-/// `self.length() < total_width`. The number of unicode characters in
-/// the returned string is `total_width` if padding is added.
+/// `self.length() < total_width`. The threshold and the pad count are
+/// measured in UTF-16 code units: `total_width - self.length()` copies of
+/// `padding_char` are appended. Characters outside the BMP count as two
+/// code units, so with such characters in `self` the result has fewer than
+/// `total_width` characters, and with a non-BMP `padding_char` the result's
+/// UTF-16 length exceeds `total_width`.
pub fn StringView::pad_end(
self : StringView,
total_width : Int,
@@ -1009,8 +1530,12 @@ pub fn StringView::pad_end(
///|
/// Returns a new string with `padding_char`s appended to `self` if
-/// `self.length() < total_width`. The number of unicode characters in
-/// the returned string is `total_width` if padding is added.
+/// `self.length() < total_width`. The threshold and the pad count are
+/// measured in UTF-16 code units: `total_width - self.length()` copies of
+/// `padding_char` are appended. Characters outside the BMP count as two
+/// code units, so with such characters in `self` the result has fewer than
+/// `total_width` characters, and with a non-BMP `padding_char` the result's
+/// UTF-16 length exceeds `total_width`.
pub fn String::pad_end(
self : String,
total_width : Int,
@@ -1875,7 +2400,9 @@ fn ascii_lowercase_copy(view : StringView, first_uppercase : Int) -> String {
}
///|
-/// Converts this string to lowercase.
+/// Converts the ASCII uppercase letters (`'A'` to `'Z'`) in this string to
+/// lowercase. All other characters, including non-ASCII letters, are left
+/// unchanged.
#cfg(not(target="js"))
pub fn StringView::to_lower(self : StringView) -> StringView {
// TODO: deal with non-ascii characters
@@ -1889,7 +2416,9 @@ pub fn StringView::to_lower(self : StringView) -> StringView {
}
///|
-/// Converts this string to lowercase.
+/// Converts the ASCII uppercase letters (`'A'` to `'Z'`) in this string to
+/// lowercase. All other characters, including non-ASCII letters, are left
+/// unchanged.
#cfg(not(target="js"))
pub fn String::to_lower(self : String) -> String {
// TODO: deal with non-ascii characters
@@ -1923,7 +2452,9 @@ pub fn StringView::to_lower(self : StringView) -> StringView {
}
///|
-/// Converts this string to lowercase.
+/// Converts the ASCII uppercase letters (`'A'` to `'Z'`) in this string to
+/// lowercase. All other characters, including non-ASCII letters, are left
+/// unchanged.
#cfg(target="js")
pub fn String::to_lower(self : String) -> String {
// TODO: deal with non-ascii characters
@@ -1987,7 +2518,9 @@ test "View::to_lower" {
}
///|
-/// Converts this string to uppercase.
+/// Converts the ASCII lowercase letters (`'a'` to `'z'`) in this string to
+/// uppercase. All other characters, including non-ASCII letters, are left
+/// unchanged.
pub fn StringView::to_upper(self : StringView) -> StringView {
// TODO: deal with non-ascii characters
guard self.find_by(c => c.is_ascii_lowercase()) is Some(idx) else {
@@ -2007,7 +2540,9 @@ pub fn StringView::to_upper(self : StringView) -> StringView {
}
///|
-/// Converts this string to uppercase.
+/// Converts the ASCII lowercase letters (`'a'` to `'z'`) in this string to
+/// uppercase. All other characters, including non-ASCII letters, are left
+/// unchanged.
pub fn String::to_upper(self : String) -> String {
// TODO: deal with non-ascii characters
guard self.find_by(c => c.is_ascii_lowercase()) is Some(idx) else {
@@ -2165,6 +2700,7 @@ test "rev_fold with raise" {
///|
/// Returns the UTF-16 code unit at the given index. Returns `None` if the index
/// is out of bounds.
+#intrinsic("%string.get_opt")
pub fn String::get(self : String, idx : Int) -> UInt16? {
guard idx >= 0 && idx < self.length() else { return None }
Some(self.unsafe_get(idx))
@@ -2173,6 +2709,7 @@ pub fn String::get(self : String, idx : Int) -> UInt16? {
///|
/// Returns the UTF-16 code unit at the given index. Returns `None` if the index
/// is out of bounds.
+#intrinsic("%stringview.get_opt")
pub fn StringView::get(self : StringView, idx : Int) -> UInt16? {
guard idx >= 0 && idx < self.length() else { return None }
Some(self.unsafe_get(idx))
diff --git a/builtin/string_test.mbt b/builtin/string_test.mbt
index 6d5690722d..2fc94eea1e 100644
--- a/builtin/string_test.mbt
+++ b/builtin/string_test.mbt
@@ -162,7 +162,7 @@ test "String default and to_array" {
///|
test "String from_iter" {
- let iter = ['a', 'b', 'c'].iter()
+ let iter = [|'a', 'b', 'c'|]
inspect(String::from_iter(iter), content="abc")
}
diff --git a/builtin/stringbuilder_buffer.mbt b/builtin/stringbuilder_buffer.mbt
index 9d79ad1d62..be3dfe68a1 100644
--- a/builtin/stringbuilder_buffer.mbt
+++ b/builtin/stringbuilder_buffer.mbt
@@ -29,11 +29,11 @@ struct StringBuilder {
///
/// Returns a new `StringBuilder` instance with the specified initial capacity.
///
-#alias(new)
+#alias(new, deprecated="Use `StringBuilder()` instead")
pub fn StringBuilder::StringBuilder(size_hint? : Int = 0) -> StringBuilder {
let initial = if size_hint < 1 { 1 } else { (size_hint + 1) / 2 }
let data : FixedArray[UInt16] = FixedArray::make(initial, 0)
- { data, len: 0 }
+ { data, len: 0, }
}
///|
@@ -139,14 +139,12 @@ pub impl Logger for StringBuilder with fn write_char(self, ch) {
}
///|
-/// Writes a part of the given string to the StringBuilder.
-///
+/// Writes a string view to the StringBuilder.
+///
/// Parameters:
///
/// * `self` : The StringBuilder to write to.
-/// * `str` : The given string.
-/// * `start` : The start index of the substring to write.
-/// * `len` : The length of the substring to write.
+/// * `str` : The view of the string to write.
///
/// Example:
///
diff --git a/builtin/stringbuilder_concat.mbt b/builtin/stringbuilder_concat.mbt
index f8e1d752bb..d8ea0df2a7 100644
--- a/builtin/stringbuilder_concat.mbt
+++ b/builtin/stringbuilder_concat.mbt
@@ -28,10 +28,10 @@ struct StringBuilder {
///
/// Returns a new `StringBuilder` instance with the specified initial capacity.
///
-#alias(new)
+#alias(new, deprecated="Use `StringBuilder()` instead")
pub fn StringBuilder::StringBuilder(size_hint? : Int = 0) -> StringBuilder {
ignore(size_hint)
- { val: "" }
+ { val: "", }
}
///|
@@ -53,14 +53,12 @@ pub impl Logger for StringBuilder with fn write_char(self, ch) {
}
///|
-/// Writes a part of the given string to the StringBuilder.
-///
+/// Writes a string view to the StringBuilder.
+///
/// Parameters:
///
/// * `self` : The StringBuilder to write to.
-/// * `str` : The given string.
-/// * `start` : The start index of the substring to write.
-/// * `len` : The length of the substring to write.
+/// * `str` : The view of the string to write.
///
/// Example:
///
diff --git a/builtin/stringview.mbt b/builtin/stringview.mbt
index 815dc12018..739f951185 100644
--- a/builtin/stringview.mbt
+++ b/builtin/stringview.mbt
@@ -151,7 +151,7 @@ pub fn StringView::code_units(self : StringView) -> ArrayView[UInt16] {
/// index is within bounds.
///
/// This method has O(1) complexity.
-/// #Example
+/// # Example
///
/// ```mbt check
/// test {
diff --git a/builtin/stringview_test.mbt b/builtin/stringview_test.mbt
index 42861e21c7..b6ab5a9167 100644
--- a/builtin/stringview_test.mbt
+++ b/builtin/stringview_test.mbt
@@ -109,12 +109,12 @@ test "StringView core operations" {
let buf = StringBuilder()
let it = view.iter2()
while it.next() is Some((i, ch)) {
- buf.write_string("\{i}:\{ch};")
+ buf <+ "\{i}:\{ch};"
}
inspect(buf.to_string(), content="0:b;1:😀;2:c;")
let from_arr = StringView::from_array(['x', 'y'])
inspect(from_arr.to_owned(), content="xy")
- let from_iter = StringView::from_iter(['m', 'n'].iter())
+ let from_iter = StringView::from_iter([|'m', 'n'|])
inspect(from_iter.to_owned(), content="mn")
let make_view = StringView::make(3, 'a')
inspect(make_view.to_owned(), content="aaa")
diff --git a/builtin/to_string_test.mbt b/builtin/to_string_test.mbt
index 1aa8de46a2..815ec49321 100644
--- a/builtin/to_string_test.mbt
+++ b/builtin/to_string_test.mbt
@@ -110,7 +110,7 @@ test "panic UInt::to_string invalid radix" {
///|
test "to_string runtime zero paths" {
- let arr = Array::new()
+ let arr = Array()
arr.push(1)
ignore(arr.pop())
let zero = arr.length()
@@ -122,7 +122,7 @@ test "to_string runtime zero paths" {
///|
test "panic UInt64::to_string invalid radix" {
- let arr = Array::new()
+ let arr = Array()
arr.push(1)
ignore(arr.pop())
let zero = arr.length().to_uint64()
diff --git a/builtin/traits_test.mbt b/builtin/traits_test.mbt
index 7432908c97..5b51bd80fc 100644
--- a/builtin/traits_test.mbt
+++ b/builtin/traits_test.mbt
@@ -29,7 +29,7 @@ priv struct Data {
///|
test "hash_data" {
- let data = Data::{ x: [1, 2, 3], y: [4, 5, 6] }
+ let data = Data::{ x: [1, 2, 3], y: [4, 5, 6], }
let map = Map([])
map[data] = 1
debug_inspect(
@@ -70,21 +70,21 @@ impl Logger for TestLogger with fn write_view(self, value) {
test "Eq helpers" {
inspect(Eq::not_equal(1, 2), content="true")
inspect(Eq::equal(3, 3), content="true")
- let a : Pair = { left: 1, right: 2 }
- let b : Pair = { left: 1, right: 3 }
+ let a : Pair = { left: 1, right: 2, }
+ let b : Pair = { left: 1, right: 3, }
inspect(Eq::not_equal(a, b), content="true")
}
///|
test "Eq::equal on custom impl" {
- let a = Wrapper::{ value: 1 }
- let b = Wrapper::{ value: 2 }
+ let a = Wrapper::{ value: 1, }
+ let b = Wrapper::{ value: 2, }
inspect(Eq::equal(a, b), content="false")
}
///|
test "Logger defaults" {
- let logger = TestLogger::{ output: "" }
+ let logger = TestLogger::{ output: "", }
Logger::write_string(logger, "hi")
Logger::write_char(logger, '!')
inspect(logger.output, content="hi!")
diff --git a/builtin/tuple_show_test.mbt b/builtin/tuple_show_test.mbt
index d7553fadfa..c9a1eee782 100644
--- a/builtin/tuple_show_test.mbt
+++ b/builtin/tuple_show_test.mbt
@@ -13,118 +13,133 @@
// limitations under the License.
///|
-#warnings("-deprecated")
-test "2-tuple to_json" {
+test "2-tuple Debug" {
let pair = (42, "hello")
- @json.json_inspect(pair.to_string(), content="(42, hello)")
+ debug_inspect(pair, content="(42, \"hello\")")
}
///|
-#warnings("-deprecated")
-test "3-tuple to_json" {
+test "3-tuple Debug" {
let triple = (42, "hello", true)
- @json.json_inspect(triple.to_string(), content="(42, hello, true)")
+ debug_inspect(triple, content="(42, \"hello\", true)")
}
///|
-#warnings("-deprecated")
-test "4-tuple to_json" {
+test "4-tuple Debug" {
let tuple = (42, "hello", true, 3.14)
- @json.json_inspect(tuple.to_string(), content="(42, hello, true, 3.14)")
+ debug_inspect(tuple, content="(42, \"hello\", true, 3.14)")
}
///|
-#warnings("-deprecated")
-test "5-tuple to_json" {
+test "5-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a')
- @json.json_inspect(tuple.to_string(), content="(42, hello, true, 3.14, a)")
+ debug_inspect(tuple, content="(42, \"hello\", true, 3.14, 'a')")
}
///|
-#warnings("-deprecated")
-test "6-tuple to_json" {
+test "6-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a', 1)
- @json.json_inspect(tuple.to_string(), content="(42, hello, true, 3.14, a, 1)")
+ debug_inspect(tuple, content="(42, \"hello\", true, 3.14, 'a', 1)")
}
///|
-#warnings("-deprecated")
-test "7-tuple to_json" {
+test "7-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a', 1, "world")
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world)",
- )
+ debug_inspect(tuple, content="(42, \"hello\", true, 3.14, 'a', 1, \"world\")")
}
///|
-#warnings("-deprecated")
-test "8-tuple to_json" {
+test "8-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a', 1, "world", false)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false)",
+ debug_inspect(
+ tuple,
+ content="(42, \"hello\", true, 3.14, 'a', 1, \"world\", false)",
)
}
///|
-#warnings("-deprecated")
-test "9-tuple to_json" {
+test "9-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a', 1, "world", false, 2.71)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71)",
+ debug_inspect(
+ tuple,
+ content="(42, \"hello\", true, 3.14, 'a', 1, \"world\", false, 2.71)",
)
}
///|
-#warnings("-deprecated")
-test "10-tuple to_json" {
+test "10-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a', 1, "world", false, 2.71, 'b')
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b)",
+ debug_inspect(
+ tuple,
+ content="(42, \"hello\", true, 3.14, 'a', 1, \"world\", false, 2.71, 'b')",
)
}
///|
-#warnings("-deprecated")
-test "11-tuple to_json" {
+test "11-tuple Debug" {
let tuple = (42, "hello", true, 3.14, 'a', 1, "world", false, 2.71, 'b', 43UL)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b, 43)",
+ debug_inspect(
+ tuple,
+ content="(42, \"hello\", true, 3.14, 'a', 1, \"world\", false, 2.71, 'b', 43)",
)
}
///|
-#warnings("-deprecated")
-test "12-tuple to_json" {
+test "12-tuple Debug" {
let tuple = (
42, "hello", true, 3.14, 'a', 1, "world", false, 2.71, 'b', 43UL, 0x12345678U,
)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b, 43, 305419896)",
+ debug_inspect(
+ tuple,
+ content=(
+ #|(
+ #| 42,
+ #| "hello",
+ #| true,
+ #| 3.14,
+ #| 'a',
+ #| 1,
+ #| "world",
+ #| false,
+ #| 2.71,
+ #| 'b',
+ #| 43,
+ #| 305419896,
+ #|)
+ ),
)
}
///|
-#warnings("-deprecated")
-test "13-tuple to_json" {
+test "13-tuple Debug" {
let tuple = (
42, "hello", true, 3.14, 'a', 1, "world", false, 2.71, 'b', 43UL, 0x12345678U,
0x87654321L,
)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b, 43, 305419896, 2271560481)",
+ debug_inspect(
+ tuple,
+ content=(
+ #|(
+ #| 42,
+ #| "hello",
+ #| true,
+ #| 3.14,
+ #| 'a',
+ #| 1,
+ #| "world",
+ #| false,
+ #| 2.71,
+ #| 'b',
+ #| 43,
+ #| 305419896,
+ #| 2271560481,
+ #|)
+ ),
)
}
///|
-#warnings("-deprecated")
-test "14-tuple to_json" {
+test "14-tuple Debug" {
let tuple = (
42,
"hello",
@@ -141,15 +156,31 @@ test "14-tuple to_json" {
0x87654321L,
(0xabcdef, 1234567890UL),
)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b, 43, 305419896, 2271560481, (11259375, 1234567890))",
+ debug_inspect(
+ tuple,
+ content=(
+ #|(
+ #| 42,
+ #| "hello",
+ #| true,
+ #| 3.14,
+ #| 'a',
+ #| 1,
+ #| "world",
+ #| false,
+ #| 2.71,
+ #| 'b',
+ #| 43,
+ #| 305419896,
+ #| 2271560481,
+ #| (11259375, 1234567890),
+ #|)
+ ),
)
}
///|
-#warnings("-deprecated")
-test "15-tuple to_json" {
+test "15-tuple Debug" {
let tuple = (
42,
"hello",
@@ -167,15 +198,32 @@ test "15-tuple to_json" {
(0xabcdef, 1234567890UL),
(0x12345678UL, 0x87654321UL, 0xabcdefUL),
)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b, 43, 305419896, 2271560481, (11259375, 1234567890), (305419896, 2271560481, 11259375))",
+ debug_inspect(
+ tuple,
+ content=(
+ #|(
+ #| 42,
+ #| "hello",
+ #| true,
+ #| 3.14,
+ #| 'a',
+ #| 1,
+ #| "world",
+ #| false,
+ #| 2.71,
+ #| 'b',
+ #| 43,
+ #| 305419896,
+ #| 2271560481,
+ #| (11259375, 1234567890),
+ #| (305419896, 2271560481, 11259375),
+ #|)
+ ),
)
}
///|
-#warnings("-deprecated")
-test "16-tuple to_json" {
+test "16-tuple Debug" {
let tuple = (
42,
"hello",
@@ -194,8 +242,27 @@ test "16-tuple to_json" {
(0x12345678UL, 0x87654321UL, 0xabcdefUL),
("wow", [0x87654321UL, 1234567890UL]),
)
- @json.json_inspect(
- tuple.to_string(),
- content="(42, hello, true, 3.14, a, 1, world, false, 2.71, b, 43, 305419896, 2271560481, (11259375, 1234567890), (305419896, 2271560481, 11259375), (wow, [2271560481, 1234567890]))",
+ debug_inspect(
+ tuple,
+ content=(
+ #|(
+ #| 42,
+ #| "hello",
+ #| true,
+ #| 3.14,
+ #| 'a',
+ #| 1,
+ #| "world",
+ #| false,
+ #| 2.71,
+ #| 'b',
+ #| 43,
+ #| 305419896,
+ #| 2271560481,
+ #| (11259375, 1234567890),
+ #| (305419896, 2271560481, 11259375),
+ #| ("wow", [2271560481, 1234567890]),
+ #|)
+ ),
)
}
diff --git a/builtin/uint.mbt b/builtin/uint.mbt
index 662279c136..3c9bc2c4d6 100644
--- a/builtin/uint.mbt
+++ b/builtin/uint.mbt
@@ -26,7 +26,24 @@
pub fn UInt::UInt(self : UInt) -> UInt = "%identity"
///|
-/// Returns the minimum of two unsigned integers.
+/// Returns the smaller of two unsigned integers.
+///
+/// Parameters:
+///
+/// * `self` : The first integer to compare.
+/// * `other` : The second integer to compare.
+///
+/// Returns `self` if it is not greater than `other`, `other` otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// inspect(UInt(1).min(UInt(2)), content="1")
+/// inspect(UInt(2).min(UInt(1)), content="1")
+/// inspect(UInt(0).min(UInt(0)), content="0")
+/// }
+/// ```
pub fn UInt::min(self : UInt, other : UInt) -> UInt {
if self < other {
self
@@ -36,7 +53,24 @@ pub fn UInt::min(self : UInt, other : UInt) -> UInt {
}
///|
-/// Returns the maximum of two unsigned integers.
+/// Returns the larger of two unsigned integers.
+///
+/// Parameters:
+///
+/// * `self` : The first integer to compare.
+/// * `other` : The second integer to compare.
+///
+/// Returns `self` if it is not less than `other`, `other` otherwise.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// inspect(UInt(1).max(UInt(2)), content="2")
+/// inspect(UInt(2).max(UInt(1)), content="2")
+/// inspect(UInt(0).max(UInt(0)), content="0")
+/// }
+/// ```
pub fn UInt::max(self : UInt, other : UInt) -> UInt {
if self > other {
self
@@ -46,7 +80,26 @@ pub fn UInt::max(self : UInt, other : UInt) -> UInt {
}
///|
-/// Clamps the value `self` between `min` and `max`.
+/// Clamps an unsigned integer into the inclusive range [`min`, `max`].
+///
+/// Parameters:
+///
+/// * `self` : The value to clamp.
+/// * `min` : The lower bound of the range.
+/// * `max` : The upper bound of the range.
+///
+/// Returns `min` if `self` is less than `min`, `max` if `self` is greater
+/// than `max`, and `self` otherwise. Aborts if `min` is greater than `max`.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// inspect(UInt(5).clamp(min=UInt(0), max=UInt(10)), content="5")
+/// inspect(UInt(0).clamp(min=UInt(2), max=UInt(10)), content="2")
+/// inspect(UInt(15).clamp(min=UInt(0), max=UInt(10)), content="10")
+/// }
+/// ```
pub fn UInt::clamp(self : UInt, min~ : UInt, max~ : UInt) -> UInt {
guard! min <= max
if self < min {
diff --git a/builtin/uint64.mbt b/builtin/uint64.mbt
index 4ee01dbfb0..f42fabd63f 100644
--- a/builtin/uint64.mbt
+++ b/builtin/uint64.mbt
@@ -36,7 +36,29 @@ test {
}
///|
-/// Converts the UInt64 to a Bytes in big-endian byte order.
+/// Converts the `UInt64` to a `Bytes` of 8 bytes in big-endian byte order
+/// (most significant byte first).
+///
+/// Parameters:
+///
+/// * `self` : The 64-bit unsigned integer to convert.
+///
+/// Returns a `Bytes` of length 8 whose first element is the most significant
+/// byte of `self` and whose last element is the least significant byte.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// // 0x41..0x48 are the ASCII codes for 'A'..'H'
+/// inspect(
+/// 0x4142_4344_4546_4748UL.to_be_bytes(),
+/// content=(
+/// #|b"ABCDEFGH"
+/// ),
+/// )
+/// }
+/// ```
pub fn UInt64::to_be_bytes(self : UInt64) -> Bytes {
[
(self >> 56).to_byte(),
@@ -51,7 +73,29 @@ pub fn UInt64::to_be_bytes(self : UInt64) -> Bytes {
}
///|
-/// Converts the UInt64 to a Bytes in little-endian byte order.
+/// Converts the `UInt64` to a `Bytes` of 8 bytes in little-endian byte order
+/// (least significant byte first).
+///
+/// Parameters:
+///
+/// * `self` : The 64-bit unsigned integer to convert.
+///
+/// Returns a `Bytes` of length 8 whose first element is the least significant
+/// byte of `self` and whose last element is the most significant byte.
+///
+/// Example:
+///
+/// ```mbt check
+/// test {
+/// // The same value as `to_be_bytes`, with the byte order reversed
+/// inspect(
+/// 0x4142_4344_4546_4748UL.to_le_bytes(),
+/// content=(
+/// #|b"HGFEDCBA"
+/// ),
+/// )
+/// }
+/// ```
pub fn UInt64::to_le_bytes(self : UInt64) -> Bytes {
[
self.to_byte(),
diff --git a/builtin/uninitialized_array.mbt b/builtin/uninitialized_array.mbt
index ec2329680b..2397ca0cd1 100644
--- a/builtin/uninitialized_array.mbt
+++ b/builtin/uninitialized_array.mbt
@@ -96,7 +96,7 @@ pub fn[T] UninitializedArray::sub(
Some(end) | (None with end = len) => end
}
guard start >= 0 && start <= end && end <= len else {
- abort("View start index out of bounds")
+ abort("View index out of bounds")
}
ArrayView::make(self, start, end - start)
}
diff --git a/bytes/README.mbt.md b/bytes/README.mbt.md
index 0d1d8c7fea..fc5157d57a 100644
--- a/bytes/README.mbt.md
+++ b/bytes/README.mbt.md
@@ -1,6 +1,6 @@
# `bytes`
-This package provides utilities for working with sequences of bytes, offering both mutable (`Bytes`) and immutable (`View`) representations.
+This package provides utilities for working with sequences of bytes, offering both an owned representation (`Bytes`) and a slice representation (`BytesView`); both are immutable.
## Creating Bytes
diff --git a/bytes/bytes_test.mbt b/bytes/bytes_test.mbt
index 49d524da8d..38a4c4b1d7 100644
--- a/bytes/bytes_test.mbt
+++ b/bytes/bytes_test.mbt
@@ -131,7 +131,7 @@ test "to_array" {
///|
test "from_iter multiple elements" {
debug_inspect(
- Bytes::from_iter([b'\x00', b'\x01', b'\x02'].iter()).to_array(),
+ Bytes::from_iter([|b'\x00', b'\x01', b'\x02'|]).to_array(),
content=(
#|[0x00, 0x01, 0x02]
),
@@ -141,7 +141,7 @@ test "from_iter multiple elements" {
///|
test "from_iter single element" {
debug_inspect(
- Bytes::from_iter([b'\x00'].iter()).to_array(),
+ Bytes::from_iter([|b'\x00'|]).to_array(),
content=(
#|[0x00]
),
@@ -150,7 +150,7 @@ test "from_iter single element" {
///|
test "from_iter empty iterator" {
- debug_inspect(Bytes::from_iter(Iter::empty()).to_array(), content="[]")
+ debug_inspect(Bytes::from_iter([||]).to_array(), content="[]")
}
///|
@@ -230,7 +230,7 @@ test "Bytes::of with different byte values" {
///|
test "Bytes::from_iter with multiple elements" {
- let iter = [b'a', b'b', b'c'].iter()
+ let iter = [|b'a', b'b', b'c'|]
let bytes = Bytes::from_iter(iter)
inspect(
bytes,
diff --git a/bytes/find_test.mbt b/bytes/find_test.mbt
index bb4bbe3229..a023259449 100644
--- a/bytes/find_test.mbt
+++ b/bytes/find_test.mbt
@@ -124,8 +124,5 @@ test "BytesView rev_find long pattern offset view" {
///|
test "Bytes::from_iter" {
- inspect(
- Bytes::from_iter(([97, 98, 99] : Array[Byte]).iter()),
- content="b\"abc\"",
- )
+ inspect(Bytes::from_iter([|97, 98, 99|]), content="b\"abc\"")
}
diff --git a/char/char_test.mbt b/char/char_test.mbt
index 1e18aa0a1e..f95bc1cd2a 100644
--- a/char/char_test.mbt
+++ b/char/char_test.mbt
@@ -44,8 +44,8 @@ priv struct TestHash {
///|
test "Char hash function with struct" {
let m = Map([])
- m[{ x: 'a' }] = '3'
- m[{ x: 'b' }] = '3'
+ m[{ x: 'a', }] = '3'
+ m[{ x: 'b', }] = '3'
debug_inspect(
m,
content=(
@@ -526,10 +526,9 @@ test "Case conversion with non-letters" {
}
///|
-#warnings("-deprecated")
-test "length is an alias of utf16_len" {
- inspect('A'.length(), content="1")
- inspect('🌟'.length(), content="2")
- assert_eq('\u{FFFF}'.length(), '\u{FFFF}'.utf16_len())
- assert_eq('\u{10000}'.length(), '\u{10000}'.utf16_len())
+test "utf16_len counts UTF-16 code units" {
+ inspect('A'.utf16_len(), content="1")
+ inspect('🌟'.utf16_len(), content="2")
+ assert_eq('\u{FFFF}'.utf16_len(), 1)
+ assert_eq('\u{10000}'.utf16_len(), 2)
}
diff --git a/char/predicate_reference_test.mbt b/char/predicate_reference_test.mbt
new file mode 100644
index 0000000000..5036c47742
--- /dev/null
+++ b/char/predicate_reference_test.mbt
@@ -0,0 +1,204 @@
+// 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.
+
+// Exhaustive equivalence check for the restructured char predicates: the
+// reference functions below are the previous single flat patterns, kept
+// verbatim, and every Unicode scalar value is compared against them. Any
+// off-by-one in the restructured fast paths (a shifted range bound, a
+// dropped table entry) fails here with the exact code point.
+
+///|
+fn reference_is_ascii_whitespace(c : Char) -> Bool {
+ c is ('\u{20}' | '\u{09}' | '\u{0A}' | '\u{0B}' | '\u{0C}' | '\u{0D}')
+}
+
+///|
+fn reference_is_whitespace(c : Char) -> Bool {
+ c
+ is ('\u0009'..='\u000D'
+ | '\u0020'
+ | '\u0085'
+ | '\u00A0'
+ | '\u1680'
+ | '\u2000'..='\u200A'
+ | '\u2028'
+ | '\u2029'
+ | '\u202F'
+ | '\u205F'
+ | '\u3000')
+}
+
+///|
+fn reference_is_numeric(c : Char) -> Bool {
+ c
+ is ('\u0030'..='\u0039'
+ | '\u00B2'
+ | '\u00B3'
+ | '\u00B9'
+ | '\u00BC'
+ | '\u00BD'
+ | '\u00BE'
+ | '\u0660'..='\u0669'
+ | '\u06F0'..='\u06F9'
+ | '\u07C0'..='\u07F9'
+ | '\u0966'..='\u096F'
+ | '\u09E6'..='\u09EF'
+ | '\u09F4'..='\u09F9'
+ | '\u0A66'..='\u0A6F'
+ | '\u0AE6'..='\u0AEF'
+ | '\u0B66'..='\u0B6F'
+ | '\u0B72'..='\u0B77'
+ | '\u0BE6'..='\u0BEF'
+ | '\u0BF0'..='\u0BF2'
+ | '\u0C66'..='\u0C6F'
+ | '\u0C78'..='\u0C7E'
+ | '\u0CE6'..='\u0CEF'
+ | '\u0D58'..='\u0D5E'
+ | '\u0D66'..='\u0D6F'
+ | '\u0D70'..='\u0D78'
+ | '\u0DE6'..='\u0DEF'
+ | '\u0E50'..='\u0E59'
+ | '\u0ED0'..='\u0ED9'
+ | '\u0F20'..='\u0F33'
+ | '\u1040'..='\u1049'
+ | '\u1090'..='\u1099'
+ | '\u1369'..='\u137C'
+ | '\u16EE'..='\u16F0'
+ | '\u17E0'..='\u17E9'
+ | '\u17F0'..='\u17F9'
+ | '\u1810'..='\u1819'
+ | '\u1946'..='\u194F'
+ | '\u19D0'..='\u19DA'
+ | '\u1A80'..='\u1A89'
+ | '\u1A90'..='\u1A99'
+ | '\u1B50'..='\u1B59'
+ | '\u1BB0'..='\u1BB9'
+ | '\u1C40'..='\u1C49'
+ | '\u1C50'..='\u1C59'
+ | '\u2070'
+ | '\u2074'..='\u2079'
+ | '\u2080'..='\u2089'
+ | '\u2150'..='\u2189'
+ | '\u2460'..='\u249B'
+ | '\u24EA'..='\u24FF'
+ | '\u2776'..='\u2793'
+ | '\u2CFD'
+ | '\u3007'
+ | '\u3021'..='\u3029'
+ | '\u3038'..='\u303A'
+ | '\u3192'..='\u3195'
+ | '\u3220'..='\u3229'
+ | '\u3248'..='\u324F'
+ | '\u3251'..='\u325F'
+ | '\u3280'..='\u3289'
+ | '\u32B1'..='\u32BF'
+ | '\uA620'..='\uA629'
+ | '\uA6E6'..='\uA6EF'
+ | '\uA830'..='\uA835'
+ | '\uA8D0'..='\uA8D9'
+ | '\uA900'..='\uA909'
+ | '\uA9D0'..='\uA9D9'
+ | '\uA9F0'..='\uA9F9'
+ | '\uAA50'..='\uAA59'
+ | '\uABF0'..='\uABF9'
+ | '\uFF10'..='\uFF19'
+ | '\u{10107}'..='\u{10133}'
+ | '\u{10140}'..='\u{10178}'
+ | '\u{1018A}'..='\u{1018B}'
+ | '\u{102E1}'..='\u{102FB}'
+ | '\u{10320}'..='\u{10323}'
+ | '\u{10341}'
+ | '\u{1034A}'
+ | '\u{103D1}'..='\u{103D5}'
+ | '\u{104A0}'..='\u{104A9}'
+ | '\u{10858}'..='\u{1085F}'
+ | '\u{10879}'..='\u{1087F}'
+ | '\u{108A7}'..='\u{108AF}'
+ | '\u{108FB}'..='\u{108FF}'
+ | '\u{10916}'..='\u{1091B}'
+ | '\u{109BC}'..='\u{109BD}'
+ | '\u{109C0}'..='\u{109CF}'
+ | '\u{10A40}'..='\u{10A48}'
+ | '\u{10A7D}'..='\u{10A7E}'
+ | '\u{10A9D}'..='\u{10A9F}'
+ | '\u{10AEB}'..='\u{10AEF}'
+ | '\u{10B58}'..='\u{10B5F}'
+ | '\u{10B78}'..='\u{10B7F}'
+ | '\u{10BA9}'..='\u{10BAF}'
+ | '\u{10CFA}'..='\u{10CFF}'
+ | '\u{10D30}'..='\u{10D39}'
+ | '\u{10D40}'..='\u{10D49}'
+ | '\u{10E60}'..='\u{10E7E}'
+ | '\u{10F1D}'..='\u{10F26}'
+ | '\u{10F51}'..='\u{10F54}'
+ | '\u{10FC5}'..='\u{10FCB}'
+ | '\u{11052}'..='\u{1106F}'
+ | '\u{110F0}'..='\u{110F9}'
+ | '\u{11136}'..='\u{1113F}'
+ | '\u{111D0}'..='\u{111D9}'
+ | '\u{111E1}'..='\u{111F4}'
+ | '\u{112F0}'..='\u{112F9}'
+ | '\u{11450}'..='\u{11459}'
+ | '\u{114D0}'..='\u{114D9}'
+ | '\u{11650}'..='\u{11659}'
+ | '\u{116C0}'..='\u{116C9}'
+ | '\u{116D0}'..='\u{116E3}'
+ | '\u{11730}'..='\u{1173B}'
+ | '\u{118E0}'..='\u{118F2}'
+ | '\u{11950}'..='\u{11959}'
+ | '\u{11BF0}'..='\u{11BF9}'
+ | '\u{11C50}'..='\u{11C6C}'
+ | '\u{11D50}'..='\u{11D59}'
+ | '\u{11DA0}'..='\u{11DA9}'
+ | '\u{11F50}'..='\u{11F59}'
+ | '\u{11FC0}'..='\u{11FD4}'
+ | '\u{12400}'..='\u{1246E}'
+ | '\u{16130}'..='\u{16139}'
+ | '\u{16A60}'..='\u{16A69}'
+ | '\u{16AC0}'..='\u{16AC9}'
+ | '\u{16B50}'..='\u{16B59}'
+ | '\u{16B5B}'..='\u{16B61}'
+ | '\u{16D70}'..='\u{16D79}'
+ | '\u{16D80}'..='\u{16E96}'
+ | '\u{1CCF0}'..='\u{1CCF9}'
+ | '\u{1D2C0}'..='\u{1D2F3}'
+ | '\u{1D360}'..='\u{1D378}'
+ | '\u{1D7CE}'..='\u{1D7FF}'
+ | '\u{1E140}'..='\u{1E149}'
+ | '\u{1E2F0}'..='\u{1E2F9}'
+ | '\u{1E4F0}'..='\u{1E4F9}'
+ | '\u{1E5F1}'..='\u{1E5FA}'
+ | '\u{1E8C7}'..='\u{1E8CF}'
+ | '\u{1E950}'..='\u{1E959}'
+ | '\u{1EC71}'..='\u{1ECB4}'
+ | '\u{1ED01}'..='\u{1ED3D}'
+ | '\u{1F100}'..='\u{1F10C}'
+ | '\u{1FBF0}'..='\u{1FBF9}')
+}
+
+///|
+test "restructured char predicates agree with the flat reference patterns for every scalar" {
+ for code in 0..<=0x10FFFF {
+ guard code.to_char() is Some(c) else { continue }
+ if c.is_ascii_whitespace() != reference_is_ascii_whitespace(c) {
+ fail("is_ascii_whitespace mismatch at code point \{code}")
+ }
+ if c.is_whitespace() != reference_is_whitespace(c) {
+ fail("is_whitespace mismatch at code point \{code}")
+ }
+ if c.is_numeric() != reference_is_numeric(c) {
+ fail("is_numeric mismatch at code point \{code}")
+ }
+ }
+}
diff --git a/cmp/README.mbt.md b/cmp/README.mbt.md
index 427486cf4b..0c0111ff98 100644
--- a/cmp/README.mbt.md
+++ b/cmp/README.mbt.md
@@ -61,7 +61,7 @@ test "reverse with arrays" {
## Comparison by Key
-With `@cmp.maximum_by_key()` and `@cmp.minimum_by_key()`, it is possible to compare values based on arbitrary keys derived from the them. This is particularly useful when you need to compare complex objects based on some specific aspect or field.
+With `@cmp.maximum_by_key()` and `@cmp.minimum_by_key()`, it is possible to compare values based on arbitrary keys derived from them. This is particularly useful when you need to compare complex objects based on some specific aspect or field.
```mbt check
///|
@@ -79,8 +79,8 @@ test "cmp_by_key" {
inspect(longer, content="hello")
// Compare structs by a specific field
- let alice = { name: "Alice", age: 25 }
- let bob = { name: "Bob", age: 30 }
+ let alice = { name: "Alice", age: 25, }
+ let bob = { name: "Bob", age: 30, }
let younger = @cmp.minimum_by_key(alice, bob, p => p.age)
debug_inspect(
younger,
diff --git a/cmp/cmp.mbt b/cmp/cmp.mbt
index c3504352ae..1c451053aa 100644
--- a/cmp/cmp.mbt
+++ b/cmp/cmp.mbt
@@ -171,8 +171,8 @@ pub fn[T : Compare] minmax(x : T, y : T) -> (T, T) {
}
///|
-/// Returns the minimum and maximum of two values based on a comparison
-/// function.
+/// Returns the minimum and maximum of two values based on a key extracted
+/// from each of them.
///
/// Parameters:
///
diff --git a/coverage/coverage.mbt b/coverage/coverage.mbt
index 9082aa9a3e..f1ecdd6cbc 100644
--- a/coverage/coverage.mbt
+++ b/coverage/coverage.mbt
@@ -61,7 +61,7 @@ priv struct CounterList {
///|
/// The global list of counters currently tracking.
-let counters : CounterList = { val: MNil }
+let counters : CounterList = { val: MNil, }
///|
/// Add the given counter along its ID to the tracking list.
diff --git a/debug/README.mbt.md b/debug/README.mbt.md
index 7dde185e3a..ca2c24e283 100644
--- a/debug/README.mbt.md
+++ b/debug/README.mbt.md
@@ -51,7 +51,7 @@ test "to_string" {
}
```
-Use `to_repr` in string interpolation:
+Use `Repr(value)` in string interpolation:
```mbt check
///|
diff --git a/debug/debug_coverage_test.mbt b/debug/debug_coverage_test.mbt
index 8f596d8135..2b6d80a289 100644
--- a/debug/debug_coverage_test.mbt
+++ b/debug/debug_coverage_test.mbt
@@ -64,8 +64,8 @@ test "assert_eq diff over primitives with mixed equal and differing fields" {
///|
test "assert_eq diff over records, enums and maps" {
- let p1 : CovPoint = { x: 1, y: 2.5 }
- let p2 : CovPoint = { x: 1, y: 4.5 }
+ let p1 : CovPoint = { x: 1, y: 2.5, }
+ let p2 : CovPoint = { x: 1, y: 4.5, }
assert_true(reports_diff(p1, p2))
let c1 : CovShape = Circle(radius=1)
let c2 : CovShape = Circle(radius=2)
@@ -168,7 +168,7 @@ test "depth-limited render replaces deep subtrees with ellipsis" {
assert_true(@debug.render(Repr(nested)).contains("1"))
assert_true(@debug.render(Repr(nested), max_depth=1).contains("..."))
// a depth-limited record still renders its (pruned) fields
- let p : CovPoint = { x: 7, y: 8.0 }
+ let p : CovPoint = { x: 7, y: 8.0, }
assert_true(!@debug.render(Repr(p), max_depth=1).is_empty())
// depth <= 0 is treated as 1, so max_depth=0 renders identically to max_depth=1
let r = Repr(nested)
@@ -181,13 +181,13 @@ test "debug runs over representative values (smoke)" {
// executes for primitive, collection and record values without raising.
@debug.debug(42)
@debug.debug([1, 2, 3])
- let p : CovPoint = { x: 1, y: 2.0 }
+ let p : CovPoint = { x: 1, y: 2.0, }
@debug.debug(p)
}
///|
test "Debug instance for Iter renders an omitted body" {
- let it : Iter[Int] = [1, 2, 3].iter()
+ let it : Iter[Int] = [|1, 2, 3|]
inspect(
@debug.to_string(it),
content=(
diff --git a/debug/delta.mbt b/debug/delta.mbt
index 74090300df..6460a532c4 100644
--- a/debug/delta.mbt
+++ b/debug/delta.mbt
@@ -152,9 +152,10 @@ fn diff_repr(
///
/// Optional parameters:
/// - `max_depth?`: maximum expansion depth; deeper subtrees are folded.
-/// Defaults to `4` when omitted. Values `<= 0` are treated as `1`.
-/// - `compact_threshold?`: compact-vs-multiline layout threshold (heuristic one-line vs multiline).
-/// Larger values prefer single-line output. Defaults to `30` when omitted.
+/// Defaults to `16` when omitted. Values `<= 0` are treated as `1`.
+/// - `compact_threshold?`: compact-vs-multiline layout threshold (maximum line
+/// width for keeping a node on one line).
+/// Larger values prefer single-line output. Defaults to `70` when omitted.
/// - `use_ansi?`: whether to emit ANSI color escape codes (for +/- markers).
/// Defaults to `true` when omitted.
fn pretty_print_delta(
diff --git a/debug/delta_wbtest.mbt b/debug/delta_wbtest.mbt
index 720289c121..d1b278c9a2 100644
--- a/debug/delta_wbtest.mbt
+++ b/debug/delta_wbtest.mbt
@@ -32,7 +32,7 @@ test "diff: empty containers stay Same" {
guard delta is Same(Array([]), []) else {
fail("expected Same(Array([]), [])")
}
- let empty = Empty::{ }
+ let empty = Empty::{ }
let delta = diff_repr(Debug::to_repr(empty), Debug::to_repr(empty))
guard delta is Same(Record([]), []) else {
fail("expected Same(Array([]), [])")
diff --git a/debug/docs_test.mbt b/debug/docs_test.mbt
index 2d9726c0f7..677ddb7e9a 100644
--- a/debug/docs_test.mbt
+++ b/debug/docs_test.mbt
@@ -41,7 +41,7 @@ fn List::debug(x : Self) -> Repr {
///|
test "Nested List" {
- let rcd = { answer: 1, xs: [1, 2, 3], tag: Ok("test string result") }
+ let rcd = { answer: 1, xs: [1, 2, 3], tag: Ok("test string result"), }
fn repeat(n) {
if n == 0 {
Nil
@@ -103,7 +103,7 @@ fn example_repr(example : Example) -> Repr {
///|
test "docs: custom type pretty_print" {
- let sample : Example = { answer: 42, xs: [1, 2, 3], tag: Ok("done") }
+ let sample : Example = { answer: 42, xs: [1, 2, 3], tag: Ok("done"), }
let r = example_repr(sample)
@builtin.inspect(
render(r),
@@ -160,7 +160,7 @@ fn numbers_repr(value : Numbers) -> Repr {
///|
test "docs: threshold controls single-line vs multi-line" {
- let sample : Numbers = { xs: [1, 2, 3] }
+ let sample : Numbers = { xs: [1, 2, 3], }
let r = numbers_repr(sample)
let compact = render(r)
let expanded = render(r)
@@ -268,7 +268,7 @@ fn dict_repr(value : DictExample) -> Repr {
///|
test "docs: dict prints as k: v pairs" {
- let sample : DictExample = { entries: [("a", 1), ("b", 2)] }
+ let sample : DictExample = { entries: [("a", 1), ("b", 2)], }
let r = dict_repr(sample)
@builtin.inspect(
render(r),
@@ -280,7 +280,7 @@ test "docs: dict prints as k: v pairs" {
///|
test "docs: dict expands with quoted keys + trailing commas" {
- let sample : DictExample = { entries: [("a", 1), ("b", 2)] }
+ let sample : DictExample = { entries: [("a", 1), ("b", 2)], }
let r = dict_repr(sample)
@builtin.inspect(
render(r),
@@ -306,7 +306,7 @@ fn opaque_repr(value : OpaqueExample) -> Repr {
///|
test "docs: opaque wrappers show tag and children" {
- let sample : OpaqueExample = { label: "Vec", xs: [1, 2] }
+ let sample : OpaqueExample = { label: "Vec", xs: [1, 2], }
let r = opaque_repr(sample)
@builtin.inspect(render(r), content="")
}
diff --git a/debug/pretty_print.mbt b/debug/pretty_print.mbt
index 145480fcfc..0943fb94e5 100644
--- a/debug/pretty_print.mbt
+++ b/debug/pretty_print.mbt
@@ -106,14 +106,11 @@ fn info_size(info : Repr) -> Int {
///|
/// Returns `true` when a record field name can be printed without quotes.
fn is_unquoted_key(key : String) -> Bool {
- key is ['a'..='z' | '_', .. rest] &&
- (for rest = rest {
- match rest {
- ['a'..='z' | 'A'..='Z' | '0'..='9' | '_', .. rest] => continue rest
- [_, ..] => break false
- [] => break true
- }
- })
+ match key {
+ ['a'..='z' | '_', .. rest] =>
+ rest.all(c => c is ('a'..='z' | 'A'..='Z' | '0'..='9' | '_'))
+ _ => false
+ }
}
///|
@@ -238,12 +235,11 @@ fn Repr::render_repr(self : Repr, threshold : Int) -> Content {
///
/// Optional parameters:
/// - `max_depth?`: maximum expansion depth; deeper subtrees are replaced with `...`.
-/// Defaults to `4` when `None`. Values `<= 0` are treated as `1`.
-/// - `compact_threshold?`: compact-vs-multiline layout threshold.
-/// The printer uses a heuristic "size" for nodes; when the structure is deemed
-/// small enough under this threshold, it is kept on one line, otherwise it is
-/// broken into multiple lines.
-/// Larger values prefer single-line output. Defaults to `80` when `None`.
+/// Defaults to `16` when `None`. Values `<= 0` are treated as `1`.
+///
+/// The compact-vs-multiline layout threshold is not configurable here: `render`
+/// always uses the internal default of `70`, keeping a node on one line when
+/// every line of its compacted form fits within that many characters.
pub fn render(r : Repr, max_depth? : Int) -> String {
let max_depth : Int? = match max_depth {
Some(_) => max_depth
diff --git a/debug/printer.mbt b/debug/printer.mbt
index a0d8688651..151d002a00 100644
--- a/debug/printer.mbt
+++ b/debug/printer.mbt
@@ -16,7 +16,7 @@
/// Rendered content with line layout and paren preference.
///
/// This is an internal implementation detail of `pretty_print`.
-/// Users should rely on `pretty_print_repr` / `pretty_print_delta`.
+/// Users should rely on `render` / `pretty_print_delta`.
priv struct Content {
size : Int
lines : Array[String]
@@ -36,31 +36,31 @@ priv struct ContentParens {
///|
impl Add for ContentParens with fn add(self, other) {
- { size: self.size + other.size, lines: self.lines + other.lines }
+ { size: self.size + other.size, lines: self.lines + other.lines, }
}
///|
/// Empty content value.
fn empty_content() -> Content {
- { size: 0, lines: [], needs_parens: false }
+ { size: 0, lines: [], needs_parens: false, }
}
///|
/// Single literal token as a `ContentParens`.
fn verbatim(x : String) -> ContentParens {
- { size: 1, lines: [x] }
+ { size: 1, lines: [x], }
}
///|
/// Build a `ContentParens` from explicit size and lines.
fn content_parens(size : Int, lines : Array[String]) -> ContentParens {
- { size, lines }
+ { size, lines, }
}
///|
/// Render any `Show` value as a leaf content node.
fn leaf(x : String, needs_parens? : Bool = false) -> Content {
- { size: 1, lines: [x], needs_parens }
+ { size: 1, lines: [x], needs_parens, }
}
///|
@@ -72,7 +72,7 @@ fn with_lines(
r : ContentParens,
f : (Array[String]) -> Array[String],
) -> ContentParens {
- { size: r.size, lines: f(r.lines) }
+ { size: r.size, lines: f(r.lines), }
}
///|
@@ -81,7 +81,7 @@ fn Content::with_lines_content(
r : Content,
f : (Array[String]) -> Array[String],
) -> Content {
- { size: r.size, lines: f(r.lines), needs_parens: r.needs_parens }
+ { size: r.size, lines: f(r.lines), needs_parens: r.needs_parens, }
}
///|
@@ -111,19 +111,19 @@ fn surround(
///|
/// Convert `Content` to `ContentParens` without adding parentheses.
fn Content::no_wrap(c : Content) -> ContentParens {
- { size: c.size, lines: c.lines }
+ { size: c.size, lines: c.lines, }
}
///|
/// Mark a content chunk as needing parentheses when embedded.
fn parens(r : ContentParens) -> Content {
- { size: r.size, lines: r.lines, needs_parens: true }
+ { size: r.size, lines: r.lines, needs_parens: true, }
}
///|
/// Mark a content chunk as not needing parentheses when embedded.
fn no_parens(r : ContentParens) -> Content {
- { size: r.size, lines: r.lines, needs_parens: false }
+ { size: r.size, lines: r.lines, needs_parens: false, }
}
///|
@@ -169,14 +169,8 @@ fn compact_lines(lines : Array[String]) -> Array[String] {
// - arrays: [a, b]
// - records/maps: { field: value, field: value }
if first == "{" {
- let s1 = match joined.strip_prefix(" ") {
- Some(sv) => sv
- None => joined
- }
- let inner = match s1.strip_suffix(" ") {
- Some(sv) => sv
- None => s1
- }
+ let s1 = joined.strip_prefix(" ").unwrap_or(joined)
+ let inner = s1.strip_suffix(" ").unwrap_or(s1)
if inner == "" {
["{}"]
} else {
@@ -272,7 +266,7 @@ fn bracket_seq_lines(
// multi-line too (important for nested structures when threshold=0).
// Also ensure the single element still gets a trailing comma in multi-line mode.
if item.length() > 1 {
- let lines = indent_spaces(indent_by, { size: 0, lines: item }).lines
+ let lines = indent_spaces(indent_by, { size: 0, lines: item, }).lines
if !lines.is_empty() {
let last_i = lines.length() - 1
lines[last_i] += ","
@@ -297,7 +291,7 @@ fn bracket_seq_lines(
_ =>
[
open,
- ..indent_spaces(indent_by, { size: 0, lines: item }).lines,
+ ..indent_spaces(indent_by, { size: 0, lines: item, }).lines,
close,
]
}
diff --git a/debug/repr.mbt b/debug/repr.mbt
index cc84715056..6e6d3a0c0f 100644
--- a/debug/repr.mbt
+++ b/debug/repr.mbt
@@ -315,24 +315,5 @@ pub fn Repr::ctor(name : String, args : Array[(String?, Repr)]) -> Repr {
/// This is used by diff/pretty-print when the structure is preserved but
/// children are rendered separately.
fn Repr::shallow(self : Repr) -> Repr {
- match self {
- UnitLit
- | Integer(_)
- | DoubleLit(_)
- | FloatLit(_)
- | BoolLit(_)
- | CharLit(_)
- | StringLit(_)
- | Literal(_)
- | Omitted => self
- Tuple(_) => Tuple([])
- Array(_) => Array([])
- Record(_) => Record([])
- Enum(name, _) => Enum(name, [])
- Opaque(name, _) => Opaque(name, Omitted)
- Map(_) => Map([])
- RecordField(name, _) => RecordField(name, Omitted)
- EnumLabeledArg(label, _) => EnumLabeledArg(label, Omitted)
- MapEntry(_, _) => MapEntry(Omitted, Omitted)
- }
+ self.with_children([])
}
diff --git a/deque/README.mbt.md b/deque/README.mbt.md
index 79e81f6cdd..c54df1a08b 100644
--- a/deque/README.mbt.md
+++ b/deque/README.mbt.md
@@ -34,7 +34,7 @@ test {
@test.assert_eq(dv.is_empty(), true)
let dv2 = @deque.from_array([1, 2, 3, 4, 5])
@test.assert_eq(dv2.length(), 5)
- let dv3 = @deque.from_iter([1, 2, 3].iter())
+ let dv3 = @deque.from_iter([|1, 2, 3|])
@test.assert_eq(dv3.length(), 3)
}
```
diff --git a/deque/deque.mbt b/deque/deque.mbt
index 06f7dcefaf..8576eee083 100644
--- a/deque/deque.mbt
+++ b/deque/deque.mbt
@@ -13,11 +13,25 @@
// limitations under the License.
///|
-fn[T] set_null(buffer : UninitializedArray[T], index : Int) = "%fixedarray.set_null"
+/// Overwrites `buf[start.. Unit {
+ for i in start.. Deque[A] {
- { buf: UninitializedArray::make(capacity), len: 0, head: 0 }
+ { buf: UninitializedArray::make(capacity), len: 0, head: 0, }
}
///|
@@ -121,7 +135,7 @@ pub impl[A] Add for Deque[A] with fn add(self, other) {
let len = self.len + other.len
let buf = self.unsafe_make_and_blit_to(len, 0)
other.unsafe_blit_to(buf, self.len)
- { buf, len, head: 0 }
+ { buf, len, head: 0, }
}
///|
@@ -173,7 +187,7 @@ pub fn[A] Deque::Deque(arr : ArrayView[A], capacity? : Int) -> Deque[A] {
for i, x in arr {
buf[i] = x
}
- { buf, len, head: 0 }
+ { buf, len, head: 0, }
}
///|
@@ -214,7 +228,7 @@ test "from_array_empty" {
pub fn[A] Deque::copy(self : Deque[A]) -> Deque[A] {
let len = self.len
let buf = self.unsafe_make_and_blit_to(len, 0)
- { buf, len, head: 0 }
+ { buf, len, head: 0, }
}
///|
@@ -492,7 +506,7 @@ pub fn[A] Deque::insert(self : Deque[A], index : Int, value : A) -> Unit {
pub fn[A] Deque::remove(self : Deque[A], index : Int) -> A {
guard index >= 0 && index < self.length() else {
abort(
- "index out of bounds: the len is from 0 to \{self.length()} but the index is \{index}",
+ "index out of bounds: the len is \{self.length()} but the index is \{index}",
)
}
let res = self[index]
@@ -505,27 +519,19 @@ pub fn[A] Deque::remove(self : Deque[A], index : Int) -> A {
let from = (self.head + i) % cap
self.buf[to] = self.buf[from]
}
- set_null(self.buf, self.head)
self.head = new_head
} else {
// Shift back elements left
- let tail_idx = (self.head + self.len - 1) % cap
for i in (index + 1).. Unit {
///|
/// Removes a front element from a deque.
///
+/// As with `Deque::pop_front`, the vacated slot goes on referring to the
+/// removed element until a later push reuses it, the buffer grows, the buffer
+/// is dropped, or `Deque::release_unused` overwrites it.
+///
/// # Example
/// ```mbt check
/// test {
@@ -683,7 +693,6 @@ pub fn[A] Deque::push_back(self : Deque[A], value : A) -> Unit {
#alias(pop_front_exn, deprecated)
pub fn[A] Deque::unsafe_pop_front(self : Deque[A]) -> Unit {
guard self.len > 0 else { abort("The deque is empty!") }
- set_null(self.buf, self.head)
let cap = self.buf.length()
self.head = (self.head + 1) % cap
self.len -= 1
@@ -701,28 +710,13 @@ test "unsafe_pop_front after many push_front" {
@test.assert_eq(dq.len, 0)
}
-///|
-/// Removes and discards the first element from the deque. This function is a
-/// deprecated version of `unsafe_pop_front`.
-///
-/// Parameters:
-///
-/// * `self` : The deque to remove the first element from.
-///
-/// Throws a runtime error if the deque is empty.
-///
-/// Example:
-///
-/// ```mbt test
-/// let dq = @deque.from_array([1, 2, 3])
-/// dq.unsafe_pop_front()
-/// inspect(dq, content="@deque.from_array([2, 3])")
-/// ```
-///
-
///|
/// Removes a back element from a deque.
///
+/// As with `Deque::pop_front`, the vacated slot goes on referring to the
+/// removed element until a later push reuses it, the buffer grows, the buffer
+/// is dropped, or `Deque::release_unused` overwrites it.
+///
/// # Example
/// ```mbt check
/// test {
@@ -736,35 +730,18 @@ test "unsafe_pop_front after many push_front" {
#alias(pop_back_exn, deprecated)
pub fn[A] Deque::unsafe_pop_back(self : Deque[A]) -> Unit {
guard self.len > 0 else { abort("The deque is empty!") }
- let tail_idx = self.tail_index()
- set_null(self.buf, tail_idx)
self.len -= 1
}
-///|
-/// Removes and discards the last element from a deque.
-///
-/// Parameters:
-///
-/// * `deque` : The deque to remove the last element from.
-///
-/// Throws a runtime error if the deque is empty.
-///
-/// Example:
-///
-/// ```mbt test
-/// let dq = @deque.from_array([1, 2, 3])
-/// // Deprecated way:
-/// // dq.pop_back_exn()
-/// // Recommended way:
-/// dq.unsafe_pop_back()
-/// inspect(dq, content="@deque.from_array([1, 2])")
-/// ```
-///
-
///|
/// Removes a front element from a deque and returns it, or `None` if it is empty.
///
+/// The vacated slot goes on referring to the removed element, so an
+/// `ArrayView` obtained from `Deque::as_views` beforehand keeps observing it,
+/// and it is released only once a later push reuses the slot, the buffer
+/// grows, or the buffer is dropped. Call `Deque::release_unused` to release it at
+/// once.
+///
/// # Example
/// ```mbt check
/// test {
@@ -775,7 +752,6 @@ pub fn[A] Deque::unsafe_pop_back(self : Deque[A]) -> Unit {
pub fn[A] Deque::pop_front(self : Deque[A]) -> A? {
guard self.len > 0 else { return None }
let value = self.buf[self.head]
- set_null(self.buf, self.head)
let cap = self.buf.length()
self.head = (self.head + 1) % cap
self.len -= 1
@@ -785,6 +761,12 @@ pub fn[A] Deque::pop_front(self : Deque[A]) -> A? {
///|
/// Removes a back element from a deque and returns it, or `None` if it is empty.
///
+/// The vacated slot goes on referring to the removed element, so an
+/// `ArrayView` obtained from `Deque::as_views` beforehand keeps observing it,
+/// and it is released only once a later push reuses the slot, the buffer
+/// grows, or the buffer is dropped. Call `Deque::release_unused` to release it at
+/// once.
+///
/// # Example
/// ```mbt check
/// test {
@@ -796,7 +778,6 @@ pub fn[A] Deque::pop_back(self : Deque[A]) -> A? {
guard self.len > 0 else { return None }
let tail_idx = self.tail_index()
let value = self.buf[tail_idx]
- set_null(self.buf, tail_idx)
self.len -= 1
Some(value)
}
@@ -879,7 +860,7 @@ pub fn[A] Deque::set(self : Deque[A], index : Int, value : A) -> Unit {
/// ```
pub fn[A] Deque::as_views(self : Deque[A]) -> (ArrayView[A], ArrayView[A]) {
guard self.len != 0 else { ([], []) }
- let { buf, head, len } = self
+ let { buf, head, len, } = self
let cap = buf.length()
let head_len = cap - head
if head_len >= len {
@@ -977,7 +958,7 @@ pub impl[A : Eq] Eq for Deque[A] with fn equal(self, other) {
/// }
/// ```
#locals(f)
-pub fn[A] Deque::each(self : Deque[A], f : (A) -> Unit) -> Unit {
+pub fn[A] Deque::each(self : Deque[A], f : (A) -> Unit raise?) -> Unit raise? {
for v in self {
f(v)
}
@@ -996,7 +977,10 @@ pub fn[A] Deque::each(self : Deque[A], f : (A) -> Unit) -> Unit {
/// }
/// ```
#locals(f)
-pub fn[A] Deque::eachi(self : Deque[A], f : (Int, A) -> Unit) -> Unit {
+pub fn[A] Deque::eachi(
+ self : Deque[A],
+ f : (Int, A) -> Unit raise?,
+) -> Unit raise? {
for i, v in self {
f(i, v)
}
@@ -1015,7 +999,10 @@ pub fn[A] Deque::eachi(self : Deque[A], f : (Int, A) -> Unit) -> Unit {
/// }
/// ```
#locals(f)
-pub fn[A] Deque::rev_each(self : Deque[A], f : (A) -> Unit) -> Unit {
+pub fn[A] Deque::rev_each(
+ self : Deque[A],
+ f : (A) -> Unit raise?,
+) -> Unit raise? {
for v in self.rev_iter() {
f(v)
}
@@ -1034,7 +1021,10 @@ pub fn[A] Deque::rev_each(self : Deque[A], f : (A) -> Unit) -> Unit {
/// }
/// ```
#locals(f)
-pub fn[A] Deque::rev_eachi(self : Deque[A], f : (Int, A) -> Unit) -> Unit {
+pub fn[A] Deque::rev_eachi(
+ self : Deque[A],
+ f : (Int, A) -> Unit raise?,
+) -> Unit raise? {
for i, v in self.rev_iter2() {
f(i, v)
}
@@ -1043,7 +1033,14 @@ pub fn[A] Deque::rev_eachi(self : Deque[A], f : (Int, A) -> Unit) -> Unit {
///|
/// Clears the deque, removing all values.
///
-/// This method has no effect on the allocated capacity of the deque, only setting the length to 0.
+/// This method has no effect on the allocated capacity of the deque, only
+/// setting the length to 0.
+///
+/// Emptying a deque is a removal like any other: the buffer keeps referring to
+/// the elements that were in it, and they are released once later pushes reuse
+/// those slots, the buffer grows, or the deque is dropped. Call
+/// `Deque::release_unused` to overwrite them at once, or `Deque::shrink_to_fit`
+/// to hand the buffer back entirely.
///
/// # Example
/// ```mbt check
@@ -1054,21 +1051,9 @@ pub fn[A] Deque::rev_eachi(self : Deque[A], f : (Int, A) -> Unit) -> Unit {
/// }
/// ```
pub fn[A] Deque::clear(self : Deque[A]) -> Unit {
- let { head, buf, len } = self
- let cap = buf.length()
- let head_len = cap - head
- if head_len >= len {
- for i in head..<(head + len) {
- set_null(buf, i)
- }
- } else {
- for i in head.. Unit {
/// }
/// ```
#locals(f)
-pub fn[A, U] Deque::map(self : Deque[A], f : (A) -> U) -> Deque[U] {
+pub fn[A, U] Deque::map(
+ self : Deque[A],
+ f : (A) -> U raise?,
+) -> Deque[U] raise? {
let cap = self.buf.length()
if self.len == 0 {
new_deque(0)
@@ -1095,7 +1083,7 @@ pub fn[A, U] Deque::map(self : Deque[A], f : (A) -> U) -> Deque[U] {
let idx = (self.head + i) % cap
buf[i] = f(self.buf[idx])
}
- { buf, len: self.len, head: 0 }
+ { buf, len: self.len, head: 0, }
}
}
@@ -1111,7 +1099,10 @@ pub fn[A, U] Deque::map(self : Deque[A], f : (A) -> U) -> Deque[U] {
/// }
/// ```
#locals(f)
-pub fn[A, U] Deque::mapi(self : Deque[A], f : (Int, A) -> U) -> Deque[U] {
+pub fn[A, U] Deque::mapi(
+ self : Deque[A],
+ f : (Int, A) -> U raise?,
+) -> Deque[U] raise? {
let cap = self.buf.length()
if self.len == 0 {
new_deque(0)
@@ -1121,7 +1112,7 @@ pub fn[A, U] Deque::mapi(self : Deque[A], f : (Int, A) -> U) -> Deque[U] {
let idx = (self.head + i) % cap
buf[i] = f(i, self.buf[idx])
}
- { buf, len: self.len, head: 0 }
+ { buf, len: self.len, head: 0, }
}
}
@@ -1224,7 +1215,7 @@ pub fn[A : Eq] Deque::contains(self : Deque[A], value : A) -> Bool {
pub fn[A] Deque::extract_if(self : Deque[A], f : (A) -> Bool) -> Deque[A] {
guard !self.is_empty() else { from_array([]) }
let removed = from_array([])
- let write = for read in 0.. Bool) -> Deque[A] {
}
continue write + 1
}
- } nobreak {
- write
- }
- let total_len = self.length()
- for i in write.. Unit {
/// inspect(dv.capacity(), content="3")
/// }
/// ```
+///
+/// The survivors are copied into the new buffer and the old one is released
+/// with them, so this also releases whatever earlier removals left in the
+/// unused capacity. It pays an allocation plus a copy of every survivor to do
+/// so; `Deque::release_unused` releases the same elements in one pass over the
+/// unused region and no allocation, at the cost of leaving the capacity alone.
pub fn[A] Deque::shrink_to_fit(self : Deque[A]) -> Unit {
if self.capacity() <= self.length() {
return
@@ -1335,6 +1326,55 @@ pub fn[A] Deque::shrink_to_fit(self : Deque[A]) -> Unit {
self.head = 0
}
+///|
+/// Overwrites the deque's unused capacity -- every slot not currently holding
+/// an element -- with `placeholder`, releasing whatever those slots held.
+///
+/// Shrinking a deque never clears the slots it vacates, so whatever they held
+/// -- a removed element, or a duplicate reference to a survivor that was
+/// shifted over it -- stays reachable from the buffer and unreleased until
+/// later pushes reuse those slots, the buffer grows, or the deque is dropped.
+/// This releases them on demand without reallocating, which is what
+/// `Deque::pop_front`, `Deque::pop_back`, `Deque::remove`, `Deque::retain`
+/// and the other operations that take no placeholder leave outstanding.
+/// `Deque::shrink_to_fit` releases them too, but by allocating an exact-size
+/// buffer and copying every survivor into it; this costs one pass over the
+/// unused region and no allocation.
+///
+/// This only matters for element types holding references -- for types such as
+/// `Int` there is nothing to release and the call merely costs a pass over the
+/// buffer.
+///
+/// # Example
+///
+/// ```mbt check
+/// test {
+/// let dq = @deque.from_array(["a", "b", "c"])
+/// let _ = dq.pop_back()
+/// dq.release_unused(placeholder="")
+/// @debug.debug_inspect(
+/// dq,
+/// content=(
+/// #|
+/// ),
+/// )
+/// }
+/// ```
+pub fn[A] Deque::release_unused(self : Deque[A], placeholder~ : A) -> Unit {
+ let cap = self.buf.length()
+ let head_len = cap - self.head
+ if self.len <= head_len {
+ // The elements sit in one run at `[head, head + len)`, so the unused
+ // capacity is everything before and after it.
+ fill_slots(self.buf, 0, self.head, placeholder)
+ fill_slots(self.buf, self.head + self.len, cap, placeholder)
+ } else {
+ // The elements wrap, occupying `[head, cap)` and `[0, len - head_len)`;
+ // what is left is the single run between them.
+ fill_slots(self.buf, self.len - head_len, self.head, placeholder)
+ }
+}
+
///|
/// Shortens the deque in-place, keeping the first `len` elements and dropping
/// the rest.
@@ -1361,34 +1401,20 @@ pub fn[A] Deque::shrink_to_fit(self : Deque[A]) -> Unit {
/// )
/// }
/// ```
+///
+/// Elements beyond `len` are removed from the deque, but the backing buffer
+/// keeps referring to them: they are released once those slots are reused,
+/// once the buffer grows, or once the deque is dropped. Call
+/// `Deque::release_unused` to overwrite them at once, or `Deque::shrink_to_fit`
+/// to move the survivors into an exact-size buffer.
pub fn[A] Deque::truncate(self : Deque[A], len : Int) -> Unit {
guard len >= 0 && len < self.len else { return }
+ // The surviving elements keep their slots, so there is nothing to move: only
+ // the length changes. An emptied deque restarts at offset zero, as
+ // `Deque::clear` and `Deque::drain` also arrange.
+ self.len = len
if len == 0 {
- self.clear()
- return
- }
- let { head, buf, .. } = self
- let (front, back) = self.as_views()
- if front.length() < len {
- // `len` is wrapping around the end of the buffer.
- // Thus, we need to drop the latter part of the back view.
- self.len = len
- let start = len - front.length()
- for i in start.. A?) -> Unit {
self.truncate(kept_len)
}
-///|
-/// Filters and maps elements in-place using a provided function. Modifies the
-/// deque to retain only elements for which the provided function returns `Some`,
-/// and updates those elements with the values inside the `Some` variant.
-///
-
///|
/// Filters elements in-place by retaining only the elements that satisfy the
/// given predicate. Modifies the deque to keep only the elements for which the
@@ -1471,7 +1491,7 @@ pub fn[A] Deque::retain_map(self : Deque[A], f : (A) -> A?) -> Unit {
/// Parameters:
///
/// * `self` : The deque to be filtered.
-/// * `predicate` : A function that takes an element and returns `true` if the
+/// * `f` : A function that takes an element and returns `true` if the
/// element should be kept, `false` if it should be removed.
///
/// Example:
@@ -1727,14 +1747,14 @@ pub fn[A] Deque::from_iter(iter : Iter[A]) -> Deque[A] {
pub fn[A] Deque::to_array(self : Deque[A]) -> Array[A] {
let len = self.length()
if len == 0 {
- []
- } else {
- let xs = Array::make(len, self[0])
- for i in 0.. Deque[A] {
} nobreak {
len
}
- let target = Deque::{ buf: UninitializedArray::make(len), len, head: 0 }
+ let target = Deque::{ buf: UninitializedArray::make(len), len, head: 0, }
for deque in self; i = 0 {
let (front, back) = deque.as_views()
target.buf.unsafe_blit(i, deque.buf, front.start_offset(), front.length())
@@ -2034,6 +2055,13 @@ pub fn[A] Deque::flatten(self : Deque[Deque[A]]) -> Deque[A] {
/// )
/// }
/// ```
+///
+/// The slots the drain vacates are not cleared: each keeps whatever it held
+/// before the survivors were shifted, so a drained element or a duplicate
+/// reference to a survivor stays reachable there until the slot is reused, the
+/// buffer grows, or the deque is dropped. Call `Deque::release_unused` to
+/// overwrite them at once, or `Deque::shrink_to_fit` to move the survivors
+/// into an exact-size buffer.
pub fn[A] Deque::drain(self : Deque[A], start~ : Int, len? : Int) -> Deque[A] {
// Validate start
guard start >= 0 && start <= self.len else {
@@ -2057,7 +2085,7 @@ pub fn[A] Deque::drain(self : Deque[A], start~ : Int, len? : Int) -> Deque[A] {
return new_deque(0)
}
// Prepare deque to return
- let deque = Deque::{ buf: UninitializedArray::make(len), len, head: 0 }
+ let deque = Deque::{ buf: UninitializedArray::make(len), len, head: 0, }
// Prepare slices
let (front, back) = self.as_views()
// We drain from front and back accordingly
@@ -2069,21 +2097,17 @@ pub fn[A] Deque::drain(self : Deque[A], start~ : Int, len? : Int) -> Deque[A] {
// copy to deque
deque.buf.unsafe_blit(0, self.buf, front.start_offset() + start, len)
if start == 0 && len == front_max_drain {
- // just set_null
- for i in front.start_offset().. Deque[A] {
// front is not empty
let back_remaining = len - front_max_drain
let back_len = back.length() - back_remaining
- if back_len == 0 {
- // back is empty
- for i in 0.. Deque[A] {
back_start + len,
back.length() - back_start - len,
)
- // set_null
- for i in (back.length() - len).. Deque[A] {
new_buf[i] = self.buf[src_idx]
}
// Create new deque with reversed elements
- { buf: new_buf, len, head: 0 }
+ { buf: new_buf, len, head: 0, }
}
///|
diff --git a/deque/deque_bench_test.mbt b/deque/deque_bench_test.mbt
new file mode 100644
index 0000000000..9368702d9d
--- /dev/null
+++ b/deque/deque_bench_test.mbt
@@ -0,0 +1,31 @@
+// 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.
+
+///|
+let deque_bench_n = 50000
+
+///|
+/// `to_array` copies every element out in index order. The Int and Ref
+/// variants are both kept because the element type decides whether the
+/// per-element store carries reference-counting traffic.
+test "bench Deque::to_array Int n=50000" (it : @bench.T) {
+ let d = @deque.from_array(Array::makei(deque_bench_n, i => i))
+ it.bench(fn() { it.keep(d.to_array()) })
+}
+
+///|
+test "bench Deque::to_array Ref n=50000" (it : @bench.T) {
+ let d = @deque.from_array(Array::makei(deque_bench_n, i => "value\{i}"))
+ it.bench(fn() { it.keep(d.to_array()) })
+}
diff --git a/deque/deque_test.mbt b/deque/deque_test.mbt
index 59fb7ce2d0..bc2e10c155 100644
--- a/deque/deque_test.mbt
+++ b/deque/deque_test.mbt
@@ -101,8 +101,7 @@ test "iter" {
dv
.iter()
.each(x => {
- buf.write_string(x.to_string())
- buf.write_char('\n')
+ buf <+ "\{x}\n"
i += 1
})
@test.assert_eq(i, dv.length())
@@ -385,6 +384,71 @@ test "map" {
)
}
+///|
+fn expect_callback_failure(action : () -> Unit raise) -> Unit raise {
+ try action() catch {
+ Failure(message) => inspect(message, content="callback failed")
+ _ => fail("unexpected error")
+ } noraise {
+ _ => fail("expected error")
+ }
+}
+
+///|
+fn fail_on_two(x : Int) -> Unit raise {
+ if x == 2 {
+ raise Failure::Failure("callback failed")
+ }
+}
+
+///|
+test "each with error callback" {
+ let dq = @deque.from_array([1, 2, 3])
+ expect_callback_failure(() => dq.each(fail_on_two))
+}
+
+///|
+test "eachi with error callback" {
+ let dq = @deque.from_array([1, 2, 3])
+ expect_callback_failure(() => dq.eachi((_i, x) => fail_on_two(x)))
+}
+
+///|
+test "rev_each with error callback" {
+ let dq = @deque.from_array([1, 2, 3])
+ expect_callback_failure(() => dq.rev_each(fail_on_two))
+}
+
+///|
+test "rev_eachi with error callback" {
+ let dq = @deque.from_array([1, 2, 3])
+ expect_callback_failure(() => dq.rev_eachi((_i, x) => fail_on_two(x)))
+}
+
+///|
+test "map with error callback" {
+ let dq = @deque.from_array([1, 2, 3])
+ expect_callback_failure(() => {
+ dq.map(x => {
+ fail_on_two(x)
+ x
+ })
+ |> ignore
+ })
+}
+
+///|
+test "mapi with error callback" {
+ let dq = @deque.from_array([1, 2, 3])
+ expect_callback_failure(() => {
+ dq.mapi((_i, x) => {
+ fail_on_two(x)
+ x
+ })
+ |> ignore
+ })
+}
+
///|
test "push_and_pop" {
let dv = @deque.from_array([1, 2, 3, 4, 5])
@@ -507,7 +571,7 @@ test "reserve_and_push" {
///|
test "from_iter multiple elements iter" {
debug_inspect(
- @deque.from_iter([1, 2, 3].iter()),
+ @deque.from_iter([|1, 2, 3|]),
content=(
#|