This is an automated email from the ASF dual-hosted git repository.

tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new a04b4120 [FEAT][RUST] Native BigInt arithmetic, ported from the C++ 
int_ops (#815)
a04b4120 is described below

commit a04b4120c449adef9008410f885a2a7c3804d167
Author: Linzhang Li <[email protected]>
AuthorDate: Sun Sep 27 23:17:56 2026 -0400

    [FEAT][RUST] Native BigInt arithmetic, ported from the C++ int_ops (#815)
    
    ## Summary
    
    `tvm_ffi::BigInt` gains arithmetic with unbounded signed integer
    semantics,
    implemented in Rust because the C ABI exposes no arithmetic entry points
    and
    the C++ operators are header-only.
    
    - Operators `+ - * / % & | ^ << >>` for owned and borrowed operands with
    `i64`
    accepted on either side, compound assignment, unary `-` and `!`. `/` and
    `%`
    truncate toward zero; bitwise operations sign-extend infinitely. Like
    the
    primitive operators, `/`, `%`, `<<` and `>>` panic on a zero divisor or
    a
      negative count.
    - Fallible forms for ffi callbacks, where a panic cannot unwind:
    `try_div`,
    `try_rem`, `div_rem`, `floor_div`, `floor_mod`, `try_shl`, `try_shr`. A
    zero
    divisor raises the new `ZERO_DIVISION_ERROR` kind, a negative shift
    count
    `ValueError`, and a shift whose result cannot be allocated
    `OverflowError`.
    - `FromStr` and `Display` in decimal, `to_f64` rounding to nearest with
    ties to
      even, and `is_zero`.
    
    `big_int/int_ops.rs` mirrors C++ `details::int_ops` in `big_int.h` one
    for
    one, so the two can be reviewed side by side: sign-extended add,
    subtract and
    negate; multiply on `u128` with the signed correction; shifts; scalar
    division
    in radix 2^32 with the normalized 64-bit divisor path; Knuth's Algorithm
    D for
    multiword division; the floor variants; and the double conversion.
    
    Results are built the way C++ builds them. Add, subtract, negate, the
    bitwise
    operations and the shifts compute each word once, so `BigInt::build`
    writes
    them straight into a fresh object and then prunes its logical size in
    place,
    like C++ `Normalize`. These objects come from `malloc` with a deleter
    that
    mirrors C++ `ArrayHandler::Deleter_`, because `free` needs no layout and
    the
    allocation may be longer than the final size. Multiplication and
    division
    accumulate into `WordsBuf`, a zeroed 32-word stack buffer that spills to
    a
    `Vec`, and finish with `from_words`, which allocates at the final length
    through `ObjectArc::new_with_extra_items`. Every object keeps the C++
    `BigIntObj` layout, so either runtime reads and releases the other's
    values.
    
    This also fixes the generic deleter behind
    `ObjectArc::new_with_extra_items`.
    When the last strong reference went while a weak reference remained, it
    saved
    the item count over the header's reference count, so the weak reference
    saw a
    live object, could lock it, and the memory was never freed. That
    affected every
    Rust-allocated `Shape`, `String`, `Bytes`, `Array` and `Tensor`, and
    `BigInt`
    once it allocates in Rust. The count now goes in the first word after
    the
    header.
    
    Benchmarks against the C++ operators on the same machine (ns/op, lower
    is
    better; both Rust builds use 64-byte function alignment so that code
    placement
    does not move the numbers):
    
    | operation | Rust | C++ |
    | --- | ---: | ---: |
    | add, 4 words + 1 | 15.8 | 15.0 |
    | multiply, 4 x 4 words | 28.9 | 36.6 |
    | divide, 4 words / 1 | 38.6 | 33.9 |
    | divide, 4 words / 4 | 60.6 | 61.7 |
    | shift left by 70 | 18.0 | 17.0 |
    | heap construction | 13.2 | 12.4 |
    | from double, 1e30 | 19.4 | 21.3 |
    | to decimal, 4 words | 192 | 627 |
    
    ## Testing
    
    - `cargo test -p tvm-ffi`: **292 passed**. `tests/test_big_int.rs` grows
    to 27
      tests, 13 of them for arithmetic: the cases ported from
    `tests/cpp/test_big_int.cc` (promotion and demotion at the `i64`
    boundary,
    mixed `i64` operands, compound assignment); signed division in every
    sign
    quadrant with inline and multiword operands and every error path;
    shift-count
    overflow and negative counts; an independent fixture with decimal
    expectations
    computed in Python; decimal parsing; Algorithm D estimate clamping, one
    and
      two corrections, add-back and the maximal normalization shift; scalar
    division boundaries including the two-correction path and `|INT64_MIN|`;
    the
      small-divisor remainder fast path against int32 boundary answers; a
    1500-round random property test (ring identities, division identities
    with
    the truncating and flooring sign rules, shifts as multiplication and
    floor
    division, text and double round trips, cross-checked against `i128`
    where it
    applies); a Rust-allocated object held and released by a C++ container
    and
    compared and hashed by C++ against a runtime-built twin; and results
    wider
      than the stack buffer.
    - Weak references: a new test in `tests/test_big_int.rs` holds a weak
    reference the way C++ `WeakObjectPtr` does across the last strong
    release of
    a `from_words` object and of an operator result, and expects an expired
    header and a freeing release; a new test in `tests/test_object.rs` does
    the
    same for a generic extra-items object and checks the layout it is freed
    with.
      Both fail before the deleter fix.
    - An offline differential run against the C++ operators: 20,000 random
    operand
    pairs of 1 to 7 words (including 0, -1, `i64::MIN` and `i64::MAX` words)
    through all 14 operations give 278,356 word-identical results, with the
    same
    inline or heap representation on both sides and the same error kinds.
    C++
    read every Rust-built heap result in place and released the last
    reference
      of 72,501 of them, with glibc heap checking on (`MALLOC_CHECK_=3`).
    - Failing the result allocation of `BigInt::from(1).try_shl(64000)` with
    an
      `LD_PRELOAD` malloc shim returns `OverflowError` instead of aborting.
    - An offline interop harness passes: Rust-built values through the C++
    `+`, `*`, `/` and `<<`, agreement of C++ `operator<<` with Rust
    `Display`,
    `ZeroDivisionError` / `TypeError` / `ValueError` crossing from C++, C++
    calling Rust callbacks with C++-built values, and reference counts back
    to
      one afterwards.
    - `rustfmt` clean on the touched files and no new `clippy` findings. No
    C++
      changes.
    
    ---------
    
    Signed-off-by: yuchuan <[email protected]>
---
 rust/tvm-ffi/src/big_int.rs         | 429 ------------------
 rust/tvm-ffi/src/big_int/int_ops.rs | 741 +++++++++++++++++++++++++++++++
 rust/tvm-ffi/src/big_int/mod.rs     | 862 ++++++++++++++++++++++++++++++++++++
 rust/tvm-ffi/src/error.rs           |   1 +
 rust/tvm-ffi/src/lib.rs             |   2 +-
 rust/tvm-ffi/src/object.rs          |  42 +-
 rust/tvm-ffi/tests/test_big_int.rs  | 738 ++++++++++++++++++++++++++++--
 rust/tvm-ffi/tests/test_object.rs   | 116 +++++
 8 files changed, 2440 insertions(+), 491 deletions(-)

diff --git a/rust/tvm-ffi/src/big_int.rs b/rust/tvm-ffi/src/big_int.rs
deleted file mode 100644
index fbcb750d..00000000
--- a/rust/tvm-ffi/src/big_int.rs
+++ /dev/null
@@ -1,429 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- */
-use crate::derive::Object;
-use crate::error::{Error, OVERFLOW_ERROR, VALUE_ERROR};
-use crate::object::{unsafe_, Object, ObjectCoreWithExtraItems};
-use crate::type_traits::AnyCompatible;
-use std::cmp::Ordering;
-use std::fmt::{Debug, Display, Write as _};
-use std::hash::{Hash, Hasher};
-use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
-use tvm_ffi_sys::{TVMFFIAny, TVMFFIAnyDataUnion, TVMFFIBigIntFromByteArray, 
TVMFFIByteArray};
-
-/// ABI stable arbitrary-precision signed integer for ffi.
-///
-/// Mirrors C++ `tvm::ffi::BigInt`: a value that fits `i64` is stored inline as
-/// `kTVMFFIInt`, and any wider value owns a `kTVMFFIBigInt` object holding its
-/// minimal two's-complement `i64` words, least-significant word first. The
-/// runtime keeps that representation canonical, so equal values share one
-/// representation and word-wise comparison decides equality.
-///
-/// The Rust side transports values across the ABI and converts them to and
-/// from native integers; arithmetic stays with the C++ implementation.
-#[repr(C)]
-pub struct BigInt {
-    data: TVMFFIAny,
-}
-
-// BigIntObj for heap-allocated integers: the word count, then the canonical
-// two's-complement words as trailing items. Mirrors C++ `details::BigIntObj`.
-// Instances are created by the runtime, which owns the normalization rules.
-#[repr(C)]
-#[derive(Object)]
-#[type_key = "ffi.BigInt"]
-#[type_index(TypeIndex::kTVMFFIBigInt)]
-#[type_final]
-pub(crate) struct BigIntObj {
-    object: Object,
-    size: usize,
-}
-
-unsafe impl ObjectCoreWithExtraItems for BigIntObj {
-    type ExtraItem = i64;
-    #[inline]
-    /// Get the count of extra items (the logical word count)
-    fn extra_items_count(this: &Self) -> usize {
-        this.size
-    }
-}
-
-impl BigInt {
-    /// Create the integer zero.
-    pub fn new() -> Self {
-        Self::from_i64(0)
-    }
-
-    #[inline]
-    fn from_i64(value: i64) -> Self {
-        Self {
-            data: TVMFFIAny {
-                type_index: TypeIndex::kTVMFFIInt as i32,
-                small_str_len: 0,
-                data_union: TVMFFIAnyDataUnion { v_int64: value },
-            },
-        }
-    }
-
-    /// Create from two's-complement `i64` words, least-significant word first.
-    ///
-    /// Redundant sign-extension words are pruned and a value that fits `i64`
-    /// is stored inline; an empty slice is zero.
-    pub fn from_words(words: &[i64]) -> Self {
-        let input = TVMFFIByteArray::new(words.as_ptr() as *const u8, 
std::mem::size_of_val(words));
-        let mut data = TVMFFIAny::new();
-        unsafe {
-            crate::check_safe_call!(TVMFFIBigIntFromByteArray(&input, &mut 
data))
-                .expect("TVMFFIBigIntFromByteArray failed");
-        }
-        Self { data }
-    }
-
-    /// Borrow the canonical two's-complement words, least-significant word 
first.
-    ///
-    /// The slice is never empty and its last word carries the sign.
-    pub fn words(&self) -> &[i64] {
-        unsafe {
-            if self.data.type_index == TypeIndex::kTVMFFIInt as i32 {
-                std::slice::from_ref(&self.data.data_union.v_int64)
-            } else {
-                let obj: &BigIntObj = &*(self.data.data_union.v_obj as *const 
BigIntObj);
-                BigIntObj::extra_items(obj)
-            }
-        }
-    }
-
-    /// Whether the value is negative.
-    pub fn is_negative(&self) -> bool {
-        let words = self.words();
-        words[words.len() - 1] < 0
-    }
-
-    /// Return the value when it fits `i64`.
-    #[inline]
-    pub fn to_i64(&self) -> Option<i64> {
-        if self.data.type_index == TypeIndex::kTVMFFIInt as i32 {
-            Some(unsafe { self.data.data_union.v_int64 })
-        } else {
-            None
-        }
-    }
-
-    /// Return the value when it fits `u64`.
-    pub fn to_u64(&self) -> Option<u64> {
-        match *self.words() {
-            [w0] => u64::try_from(w0).ok(),
-            [w0, 0] => Some(w0 as u64),
-            _ => None,
-        }
-    }
-
-    /// Return the value when it fits `i128`.
-    pub fn to_i128(&self) -> Option<i128> {
-        match *self.words() {
-            [w0] => Some(i128::from(w0)),
-            [w0, w1] => Some((i128::from(w1) << 64) | i128::from(w0 as u64)),
-            _ => None,
-        }
-    }
-
-    /// Return the value when it fits `u128`.
-    pub fn to_u128(&self) -> Option<u128> {
-        let combine = |w0: i64, w1: i64| (u128::from(w1 as u64) << 64) | 
u128::from(w0 as u64);
-        match *self.words() {
-            [w0] => u128::try_from(w0).ok(),
-            [w0, w1] if w1 >= 0 => Some(combine(w0, w1)),
-            [w0, w1, 0] => Some(combine(w0, w1)),
-            _ => None,
-        }
-    }
-}
-
-impl Default for BigInt {
-    #[inline]
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-/// Macro to implement From for integer types that always fit i64
-macro_rules! impl_big_int_from_int {
-    ($($int_type:ty),* $(,)?) => {
-        $(
-            impl From<$int_type> for BigInt {
-                #[inline]
-                fn from(value: $int_type) -> Self {
-                    Self::from_i64(value as i64)
-                }
-            }
-        )*
-    };
-}
-
-impl_big_int_from_int!(i8, i16, i32, i64, isize, u8, u16, u32);
-
-impl From<bool> for BigInt {
-    #[inline]
-    fn from(value: bool) -> Self {
-        Self::from_i64(i64::from(value))
-    }
-}
-
-impl From<u64> for BigInt {
-    fn from(value: u64) -> Self {
-        match i64::try_from(value) {
-            Ok(value) => Self::from_i64(value),
-            Err(_) => Self::from_words(&[value as i64, 0]),
-        }
-    }
-}
-
-impl From<usize> for BigInt {
-    #[inline]
-    fn from(value: usize) -> Self {
-        Self::from(value as u64)
-    }
-}
-
-impl From<i128> for BigInt {
-    fn from(value: i128) -> Self {
-        match i64::try_from(value) {
-            Ok(value) => Self::from_i64(value),
-            // The arithmetic shift keeps the sign in the high word.
-            Err(_) => Self::from_words(&[value as i64, (value >> 64) as i64]),
-        }
-    }
-}
-
-impl From<u128> for BigInt {
-    fn from(value: u128) -> Self {
-        match i64::try_from(value) {
-            Ok(value) => Self::from_i64(value),
-            // The zero guard keeps the value positive; normalization prunes 
it when redundant.
-            Err(_) => Self::from_words(&[value as i64, (value >> 64) as i64, 
0]),
-        }
-    }
-}
-
-/// Negate two's-complement words in place, modulo their fixed width.
-fn negate_in_place(words: &mut [i64]) {
-    let mut carry = true;
-    for word in words.iter_mut() {
-        let (sum, overflow) = (!(*word as 
u64)).overflowing_add(u64::from(carry));
-        *word = sum as i64;
-        carry = overflow;
-    }
-}
-
-impl TryFrom<f64> for BigInt {
-    type Error = Error;
-
-    /// Convert a finite double by truncating toward zero.
-    ///
-    /// NaN raises `ValueError` and infinity raises `OverflowError`.
-    fn try_from(value: f64) -> Result<Self, Error> {
-        // The upper bound is exclusive: i64::MAX rounds up to 2^63 as a 
double.
-        const LOWER: f64 = i64::MIN as f64;
-        if (LOWER..-LOWER).contains(&value) {
-            return Ok(Self::from_i64(value as i64));
-        }
-        if value.is_nan() {
-            return Err(Error::new(VALUE_ERROR, "Cannot convert NaN to BigInt", 
""));
-        }
-        if value.is_infinite() {
-            return Err(Error::new(
-                OVERFLOW_ERROR,
-                "Cannot convert infinity to BigInt",
-                "",
-            ));
-        }
-        // |value| >= 2^63 is an integer: place the 53-bit significand at its 
binary exponent.
-        let bits = value.to_bits();
-        let exponent = (((bits >> 52) & 0x7ff) - 1075) as usize;
-        let significand = (bits & ((1u64 << 52) - 1)) | (1u64 << 52);
-        let whole = exponent / 64;
-        let part = exponent % 64;
-        // Low zero words, up to two significand words, and a zero sign guard.
-        let mut words = vec![0i64; whole + 3];
-        words[whole] = (significand << part) as i64;
-        if part != 0 {
-            words[whole + 1] = (significand >> (64 - part)) as i64;
-        }
-        if value < 0.0 {
-            negate_in_place(&mut words);
-        }
-        Ok(Self::from_words(&words))
-    }
-}
-
-impl Clone for BigInt {
-    #[inline]
-    fn clone(&self) -> Self {
-        if self.data.type_index == TypeIndex::kTVMFFIBigInt as i32 {
-            unsafe { unsafe_::inc_ref(self.data.data_union.v_obj) }
-        }
-        Self { data: self.data }
-    }
-}
-
-impl Drop for BigInt {
-    #[inline]
-    fn drop(&mut self) {
-        if self.data.type_index == TypeIndex::kTVMFFIBigInt as i32 {
-            unsafe { unsafe_::dec_ref(self.data.data_union.v_obj) }
-        }
-    }
-}
-
-impl PartialEq for BigInt {
-    /// Canonical words make equal values word-for-word identical.
-    #[inline]
-    fn eq(&self, other: &Self) -> bool {
-        self.words() == other.words()
-    }
-}
-
-impl Eq for BigInt {}
-
-impl PartialOrd for BigInt {
-    #[inline]
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for BigInt {
-    fn cmp(&self, other: &Self) -> Ordering {
-        let (a, b) = (self.words(), other.words());
-        let (negative_a, negative_b) = (a[a.len() - 1] < 0, b[b.len() - 1] < 
0);
-        if negative_a != negative_b {
-            return if negative_a {
-                Ordering::Less
-            } else {
-                Ordering::Greater
-            };
-        }
-        // With equal signs, sign-extended words compare as unsigned from the 
top.
-        let extension = if negative_a { u64::MAX } else { 0 };
-        let word = |words: &[i64], i: usize| words.get(i).map_or(extension, 
|&w| w as u64);
-        (0..a.len().max(b.len()))
-            .rev()
-            .map(|i| word(a, i).cmp(&word(b, i)))
-            .find(|ordering| ordering.is_ne())
-            .unwrap_or(Ordering::Equal)
-    }
-}
-
-impl Hash for BigInt {
-    #[inline]
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        self.words().hash(state);
-    }
-}
-
-impl Display for BigInt {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        if let Some(value) = self.to_i64() {
-            return Display::fmt(&value, f);
-        }
-        let words = self.words();
-        let negative = words[words.len() - 1] < 0;
-        let mut magnitude = words.to_vec();
-        if negative {
-            negate_in_place(&mut magnitude);
-        }
-        // Peel base-10^19 chunks, least significant first, by long division.
-        const CHUNK: u128 = 10_000_000_000_000_000_000;
-        let mut chunks = Vec::new();
-        while magnitude.last() == Some(&0) {
-            magnitude.pop();
-        }
-        while !magnitude.is_empty() {
-            let mut remainder = 0u128;
-            for word in magnitude.iter_mut().rev() {
-                let current = (remainder << 64) | u128::from(*word as u64);
-                *word = (current / CHUNK) as i64;
-                remainder = current % CHUNK;
-            }
-            chunks.push(remainder as u64);
-            while magnitude.last() == Some(&0) {
-                magnitude.pop();
-            }
-        }
-        let mut digits = String::new();
-        let mut chunks = chunks.iter().rev();
-        // A heap value is never zero, so the leading chunk always exists.
-        write!(digits, "{}", chunks.next().unwrap_or(&0))?;
-        for chunk in chunks {
-            write!(digits, "{chunk:019}")?;
-        }
-        f.pad_integral(!negative, "", &digits)
-    }
-}
-
-impl Debug for BigInt {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ffi.BigInt")
-            .field("data", &format_args!("{self}"))
-            .finish()
-    }
-}
-
-//-----------------------------------------------------
-// AnyCompatible implementation for BigInt
-//-----------------------------------------------------
-unsafe impl AnyCompatible for BigInt {
-    fn type_str() -> std::string::String {
-        "BigInt".to_string()
-    }
-
-    unsafe fn copy_to_any_view(this: &Self, data: &mut TVMFFIAny) {
-        *data = this.data;
-    }
-
-    unsafe fn move_to_any(src: Self, data: &mut TVMFFIAny) {
-        *data = src.data;
-        std::mem::forget(src);
-    }
-
-    unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
-        data.type_index == TypeIndex::kTVMFFIInt as i32
-            || data.type_index == TypeIndex::kTVMFFIBigInt as i32
-    }
-
-    unsafe fn copy_from_any_view_after_check(data: &TVMFFIAny) -> Self {
-        if data.type_index == TypeIndex::kTVMFFIBigInt as i32 {
-            unsafe { unsafe_::inc_ref(data.data_union.v_obj) }
-        }
-        Self { data: *data }
-    }
-
-    unsafe fn move_from_any_after_check(data: &mut TVMFFIAny) -> Self {
-        Self { data: *data }
-    }
-
-    unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> Result<Self, ()> {
-        if Self::check_any_strict(data) {
-            Ok(Self::copy_from_any_view_after_check(data))
-        } else if data.type_index == TypeIndex::kTVMFFIBool as i32 {
-            Ok(Self::from_i64(data.data_union.v_int64))
-        } else {
-            Err(())
-        }
-    }
-}
diff --git a/rust/tvm-ffi/src/big_int/int_ops.rs 
b/rust/tvm-ffi/src/big_int/int_ops.rs
new file mode 100644
index 00000000..fb6bff15
--- /dev/null
+++ b/rust/tvm-ffi/src/big_int/int_ops.rs
@@ -0,0 +1,741 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+//! Word-level integer algorithms behind the `BigInt` operators.
+//!
+//! Every fallback takes canonical two's-complement words, least-significant
+//! first, and returns a canonical `BigInt`. The functions mirror C++
+//! `details::int_ops` in `include/tvm/ffi/big_int.h` one for one; keep them in
+//! sync. Word arithmetic wraps modulo 2^64 on purpose, hence the explicit
+//! `wrapping_*`/`overflowing_*` forms. As in C++, a result whose words are 
each
+//! computed once is written straight into a fresh object and pruned in place
+//! (`BigInt::build`); multiplication and division accumulate into a zeroed
+//! [`WordsBuf`] and finish with [`BigInt::from_words`].
+#![allow(clippy::needless_range_loop)]
+
+use super::BigInt;
+use crate::error::{Error, Result, OVERFLOW_ERROR, VALUE_ERROR, 
ZERO_DIVISION_ERROR};
+use std::mem::MaybeUninit;
+
+const BASE: u64 = 1 << 32;
+
+/// Words kept on the stack before a result or scratch buffer spills to the 
heap.
+const INLINE_WORDS: usize = 32;
+
+/// Result or scratch words, zeroed in place on first use: up to 
`INLINE_WORDS` on
+/// the caller's stack, longer on the heap. Two phases avoid moving the array.
+struct WordsBuf {
+    inline: MaybeUninit<[i64; INLINE_WORDS]>,
+    heap: Vec<i64>,
+}
+
+impl WordsBuf {
+    #[inline(always)]
+    fn new() -> Self {
+        Self {
+            inline: MaybeUninit::uninit(),
+            heap: Vec::new(),
+        }
+    }
+
+    /// Zero `len` words in place and expose them; like `Vec`, exhaustion 
aborts.
+    #[inline]
+    fn zeroed(&mut self, len: usize) -> &mut [i64] {
+        if len <= INLINE_WORDS {
+            return self.inline_zeroed(len);
+        }
+        self.heap.resize(len, 0);
+        &mut self.heap
+    }
+
+    #[inline]
+    fn inline_zeroed(&mut self, len: usize) -> &mut [i64] {
+        debug_assert!(len <= INLINE_WORDS);
+        let words = self.inline.as_mut_ptr().cast::<i64>();
+        // Only these `len` words are ever exposed, and they are initialized 
here.
+        unsafe {
+            std::ptr::write_bytes(words, 0, len);
+            std::slice::from_raw_parts_mut(words, len)
+        }
+    }
+}
+
+pub(super) fn zero_division() -> Error {
+    Error::new(ZERO_DIVISION_ERROR, "Division by zero", "")
+}
+
+fn negative_shift() -> Error {
+    Error::new(VALUE_ERROR, "Negative BigInt shift count", "")
+}
+
+fn overflow(message: &str) -> Error {
+    Error::new(OVERFLOW_ERROR, message, "")
+}
+
+/// Whether a nonempty signed-word view represents a negative integer.
+#[inline]
+pub(super) fn is_negative(x: &[i64]) -> bool {
+    x[x.len() - 1] < 0
+}
+
+/// The word that sign-extends `x` beyond its length.
+#[inline]
+pub(super) fn extension(x: &[i64]) -> u64 {
+    if is_negative(x) {
+        u64::MAX
+    } else {
+        0
+    }
+}
+
+/// Word `i` of `x`, sign-extended past its length.
+#[inline]
+pub(super) fn word_at(x: &[i64], i: usize, extension: u64) -> u64 {
+    x.get(i).map_or(extension, |&w| w as u64)
+}
+
+/// Length after pruning redundant sign words; zero words stay zero.
+pub(super) fn normalized_len(words: &[i64]) -> usize {
+    let mut size = words.len();
+    while size > 1 && words[size - 1] == (if words[size - 2] < 0 { -1 } else { 
0 }) {
+        size -= 1;
+    }
+    size
+}
+
+// ---- Inline (i64) forms. `Ok(None)` means the result needs the multiword 
fallback.
+
+/// Quotient of division rounded toward zero.
+#[inline]
+pub(super) fn trunc_div_i64(a: i64, b: i64) -> Result<Option<i64>> {
+    if b == 0 {
+        return Err(zero_division());
+    }
+    // i64::MIN / -1 is the only quotient that needs promotion.
+    Ok(a.checked_div(b))
+}
+
+/// Remainder of division rounded toward zero.
+#[inline]
+pub(super) fn trunc_mod_i64(a: i64, b: i64) -> Result<Option<i64>> {
+    if b == 0 {
+        return Err(zero_division());
+    }
+    // The remainder of i64::MIN / -1 is zero, which the wrapping form yields.
+    Ok(Some(a.wrapping_rem(b)))
+}
+
+/// Quotient of division rounded down.
+#[inline]
+pub(super) fn floor_div_i64(a: i64, b: i64) -> Result<Option<i64>> {
+    if b == 0 {
+        return Err(zero_division());
+    }
+    Ok(a.checked_div(b).map(|q| {
+        // A nonexact quotient of opposite signs rounds one lower.
+        if a.wrapping_rem(b) != 0 && ((a < 0) != (b < 0)) {
+            q - 1
+        } else {
+            q
+        }
+    }))
+}
+
+/// Remainder of division rounded down.
+#[inline]
+pub(super) fn floor_mod_i64(a: i64, b: i64) -> Result<Option<i64>> {
+    if b == 0 {
+        return Err(zero_division());
+    }
+    let r = a.wrapping_rem(b);
+    // Compensate for the quotient rounding down, preserving a = q*b + r.
+    Ok(Some(if r != 0 && ((a < 0) != (b < 0)) {
+        r + b
+    } else {
+        r
+    }))
+}
+
+/// Left shift, rejecting negative counts.
+#[inline]
+pub(super) fn shl_i64(a: i64, b: i64) -> Result<Option<i64>> {
+    if b < 0 {
+        return Err(negative_shift());
+    }
+    if b == 0 || a == 0 {
+        return Ok(Some(a));
+    }
+    // For positive a require a <= i64::MAX >> b; for negative a require a >= 
-2^(63-b).
+    if b >= 64 || a > (i64::MAX >> b) || a < -(1i64 << (63 - b)) {
+        return Ok(None);
+    }
+    Ok(Some(a << b))
+}
+
+/// Right shift with arithmetic sign extension; this operation cannot overflow.
+#[inline]
+pub(super) fn shr_i64(a: i64, b: i64) -> Result<i64> {
+    if b < 0 {
+        return Err(negative_shift());
+    }
+    Ok(if b >= 64 {
+        if a < 0 {
+            -1
+        } else {
+            0
+        }
+    } else {
+        a >> b
+    })
+}
+
+// ---- Multiword fallbacks.
+
+/// Add signed integers.
+pub(super) fn add(a: &[i64], b: &[i64]) -> BigInt {
+    // One sign-extension word retains the carry out until normalization.
+    let size = a.len().max(b.len()) + 1;
+    let (ext_a, ext_b) = (extension(a), extension(b));
+    let mut carry = 0u64;
+    BigInt::build(size, |i| {
+        let (sum, c1) = word_at(a, i, ext_a).overflowing_add(word_at(b, i, 
ext_b));
+        let (result, c2) = sum.overflowing_add(carry);
+        carry = u64::from(c1 | c2);
+        result as i64
+    })
+}
+
+/// Subtract signed integers: a - b = a + !b + 1 after sign extension.
+pub(super) fn sub(a: &[i64], b: &[i64]) -> BigInt {
+    let size = a.len().max(b.len()) + 1;
+    let (ext_a, ext_b) = (extension(a), extension(b));
+    let mut carry = 1u64;
+    BigInt::build(size, |i| {
+        let (sum, c1) = word_at(a, i, ext_a).overflowing_add(!word_at(b, i, 
ext_b));
+        let (result, c2) = sum.overflowing_add(carry);
+        carry = u64::from(c1 | c2);
+        result as i64
+    })
+}
+
+/// Negate a signed integer: -x = !x + 1, with room for negating a minimum 
value.
+pub(super) fn negate(x: &[i64]) -> BigInt {
+    let size = x.len() + 1;
+    let ext = extension(x);
+    let mut carry = 1u64;
+    BigInt::build(size, |i| {
+        let word = !word_at(x, i, ext);
+        let (result, overflow) = word.overflowing_add(carry);
+        carry = u64::from(overflow);
+        result as i64
+    })
+}
+
+/// Negate two's-complement words in place, modulo their fixed width.
+pub(super) fn negate_in_place(words: &mut [i64]) {
+    let mut carry = true;
+    for word in words.iter_mut() {
+        let (sum, overflow) = (!(*word as 
u64)).overflowing_add(u64::from(carry));
+        *word = sum as i64;
+        carry = overflow;
+    }
+}
+
+/// Multiply signed integers.
+pub(super) fn mul(a: &[i64], b: &[i64]) -> BigInt {
+    let (na, nb) = (a.len(), b.len());
+    let mut buf = WordsBuf::new();
+    let data = buf.zeroed(na + nb);
+    for i in 0..na {
+        let x = u128::from(a[i] as u64);
+        let mut carry = 0u128;
+        for j in 0..nb {
+            // x*y + word + carry < 2^128: each term is below its width's 
maximum.
+            let product = x * u128::from(b[j] as u64) + u128::from(data[i + j] 
as u64) + carry;
+            data[i + j] = product as u64 as i64;
+            carry = product >> 64;
+        }
+        data[i + nb] = carry as u64 as i64;
+    }
+    // With stored unsigned A/B and 0/1 negative indicators neg_a/neg_b,
+    // a = A - neg_a*2^(64*n), b = B - neg_b*2^(64*m). Thus ab is AB minus
+    // neg_a*B*2^(64*n) and neg_b*A*2^(64*m); the remaining
+    // neg_a*neg_b*2^(64*(n+m)) vanishes modulo the allocated n+m words.
+    let subtract_shifted = |data: &mut [i64], x: &[i64], shift: usize| {
+        let mut borrow = 0u64;
+        for i in 0..x.len() {
+            let word = x[i] as u64;
+            let (rhs, o1) = word.overflowing_add(borrow);
+            let (result, o2) = (data[shift + i] as u64).overflowing_sub(rhs);
+            data[shift + i] = result as i64;
+            borrow = u64::from(o1 | o2);
+        }
+    };
+    if is_negative(a) {
+        subtract_shifted(data, b, na);
+    }
+    if is_negative(b) {
+        subtract_shifted(data, a, nb);
+    }
+    BigInt::from_words(data)
+}
+
+/// Apply a sign-extended bitwise operation.
+fn bitwise(a: &[i64], b: &[i64], op: impl Fn(u64, u64) -> u64) -> BigInt {
+    let size = a.len().max(b.len());
+    let (ext_a, ext_b) = (extension(a), extension(b));
+    BigInt::build(size, |i| {
+        op(word_at(a, i, ext_a), word_at(b, i, ext_b)) as i64
+    })
+}
+
+pub(super) fn and(a: &[i64], b: &[i64]) -> BigInt {
+    bitwise(a, b, |x, y| x & y)
+}
+
+pub(super) fn or(a: &[i64], b: &[i64]) -> BigInt {
+    bitwise(a, b, |x, y| x | y)
+}
+
+pub(super) fn xor(a: &[i64], b: &[i64]) -> BigInt {
+    bitwise(a, b, |x, y| x ^ y)
+}
+
+/// Complement all sign-extended bits.
+pub(super) fn not(x: &[i64]) -> BigInt {
+    BigInt::build(x.len(), |i| !x[i])
+}
+
+/// Convert a nonnegative shift count; `None` when it does not fit `usize`.
+fn shift_count(count: &[i64]) -> Result<Option<usize>> {
+    if is_negative(count) {
+        return Err(negative_shift());
+    }
+    if count[1..].iter().any(|&w| w != 0) {
+        return Ok(None);
+    }
+    Ok(usize::try_from(count[0] as u64).ok())
+}
+
+/// Shift a signed integer left, growing as needed.
+///
+/// Inlined so callers unwrap the `Result` in registers: moving a `BigInt` out 
of a
+/// returned `Result` reloads it in one 16-byte read that store forwarding 
misses.
+#[inline]
+pub(super) fn shl(x: &[i64], count: &[i64]) -> Result<BigInt> {
+    let shift = shift_count(count)?;
+    if x.len() == 1 && x[0] == 0 {
+        return Ok(BigInt::from_i64(0));
+    }
+    let too_large = || overflow("BigInt shift count is too large");
+    let shift = shift.ok_or_else(too_large)?;
+    let whole = shift / 64;
+    let part = (shift % 64) as u32;
+    let size = x
+        .len()
+        .checked_add(whole)
+        .and_then(|size| size.checked_add(1))
+        .ok_or_else(too_large)?;
+    let ext = extension(x);
+    BigInt::try_build(size, |i| {
+        // The low `whole` words are zero.
+        if i < whole {
+            return 0;
+        }
+        let j = i - whole;
+        let mut word = word_at(x, j, ext) << part;
+        if part != 0 && j != 0 {
+            word |= (x[j - 1] as u64) >> (64 - part);
+        }
+        word as i64
+    })
+    .ok_or_else(|| overflow("BigInt allocation is too large"))
+}
+
+/// Shift a signed integer right with sign extension; huge counts give 0 or -1.
+pub(super) fn shr(x: &[i64], count: &[i64]) -> Result<BigInt> {
+    let shift = match shift_count(count)? {
+        Some(shift) if shift / 64 < x.len() => shift,
+        _ => return Ok(BigInt::from_i64(if is_negative(x) { -1 } else { 0 })),
+    };
+    let whole = shift / 64;
+    let size = x.len() - whole;
+    let part = (shift % 64) as u32;
+    let ext = extension(x);
+    let result = BigInt::build(size, |i| {
+        let mut word = (x[i + whole] as u64) >> part;
+        // The last partial word draws its high bits from the sign extension.
+        if part != 0 {
+            word |= word_at(x, i + whole + 1, ext) << (64 - part);
+        }
+        word as i64
+    });
+    Ok(result)
+}
+
+// Absolute-value access without copying operands: negation carries through the
+// low zero words, negates the first nonzero word, then complements higher 
words.
+
+/// The first nonzero index, or `x.len()` for zero.
+fn first_nonzero(x: &[i64]) -> usize {
+    x.iter().position(|&w| w != 0).unwrap_or(x.len())
+}
+
+/// Raw word `i` of the absolute value of `x`; indices beyond the view read as 
zero.
+#[inline]
+fn abs_word(x: &[i64], first_nonzero: usize, i: usize) -> i64 {
+    if i >= x.len() {
+        return 0;
+    }
+    let word = x[i];
+    if !is_negative(x) {
+        return word;
+    }
+    // Negation maps [0,0,w,v] to [0,0,-w,!v] as modulo-2^64 word patterns.
+    if i < first_nonzero {
+        0
+    } else if i == first_nonzero {
+        word.wrapping_neg()
+    } else {
+        !word
+    }
+}
+
+/// Truncating quotient and remainder with an `i64` divisor.
+///
+/// The remainder is zero or has the dividend's sign.
+pub(super) fn div_rem_single(a: &[i64], b: i64) -> Result<(BigInt, BigInt)> {
+    if b == 0 {
+        return Err(zero_division());
+    }
+    let divisor = b.unsigned_abs();
+    let first_a = first_nonzero(a);
+    let nq = a.len() + 1;
+    let mut quotient = WordsBuf::new();
+    // The top word stays zero as the sign guard.
+    let q = quotient.zeroed(nq);
+    let mut remainder = 0u64;
+    let mut shift = 0u32;
+    let mut normalized = divisor;
+    if divisor > u64::from(u32::MAX) {
+        while normalized >> 63 == 0 {
+            normalized <<= 1;
+            shift += 1;
+        }
+    }
+    let divisor_high = normalized >> 32;
+    let divisor_low = normalized as u32 as u64;
+    for i in (0..a.len()).rev() {
+        let word = abs_word(a, first_a, i) as u64;
+        let quotient_word = if divisor <= u64::from(u32::MAX) {
+            // remainder < divisor < 2^32, so each brought-down half fits u64
+            // and yields a quotient half below 2^32 while restoring the bound.
+            let high = (remainder << 32) | (word >> 32);
+            let quotient_high = high / divisor;
+            remainder = high % divisor;
+            let low = (remainder << 32) | (word as u32 as u64);
+            let quotient_low = low / divisor;
+            remainder = low % divisor;
+            (quotient_high << 32) | quotient_low
+        } else {
+            // Normalize the carried 128-bit numerator without ever shifting 
by 64.
+            let high = if shift != 0 {
+                (remainder << shift) | (word >> (64 - shift))
+            } else {
+                remainder
+            };
+            let low = word << shift;
+            // Requires upper < normalized; returns a radix-2^32 quotient digit
+            // and a remainder below normalized.
+            let digit = |upper: u64, next: u64| -> (u64, u64) {
+                let mut estimate = upper / divisor_high;
+                let mut residual = upper % divisor_high;
+                // The normalized high digit bounds overestimation by two. 
Check
+                // the base before multiplying, and stop before a residual 
shift
+                // can overflow.
+                while estimate >= BASE || estimate * divisor_low > (residual 
<< 32) + next {
+                    estimate -= 1;
+                    residual += divisor_high;
+                    if residual >= BASE {
+                        break;
+                    }
+                }
+                // Wrapped subtraction is exact: the true remainder is below 
normalized.
+                let rest = (upper << 32)
+                    .wrapping_add(next)
+                    .wrapping_sub(estimate.wrapping_mul(normalized));
+                (estimate, rest)
+            };
+            let upper = digit(high, low >> 32);
+            let lower = digit(upper.1, low as u32 as u64);
+            remainder = lower.1 >> shift;
+            (upper.0 << 32) | lower.0
+        };
+        q[i] = quotient_word as i64;
+    }
+    if is_negative(a) != (b < 0) {
+        negate_in_place(&mut q[..nq]);
+    }
+    // remainder < |b| <= 2^63 makes the conversion and signed negation safe.
+    let mut signed_remainder = remainder as i64;
+    if is_negative(a) {
+        signed_remainder = -signed_remainder;
+    }
+    Ok((BigInt::from_words(q), BigInt::from_i64(signed_remainder)))
+}
+
+/// Truncating quotient and remainder in one division pass.
+///
+/// The remainder is zero or has the dividend's sign.
+pub(super) fn div_rem(a: &[i64], b: &[i64]) -> Result<(BigInt, BigInt)> {
+    if b.len() == 1 {
+        return div_rem_single(a, b[0]);
+    }
+    let first_a = first_nonzero(a);
+    let first_b = first_nonzero(b);
+    // Count magnitude digits, dropping the signed representation's high zero 
guards.
+    let digit_count = |x: &[i64], first: usize| -> usize {
+        let mut words = x.len();
+        while words > 0 && abs_word(x, first, words - 1) == 0 {
+            words -= 1;
+        }
+        if words == 0 {
+            return 0;
+        }
+        let top = abs_word(x, first, words - 1) as u64;
+        2 * words - usize::from(top >> 32 == 0)
+    };
+    let m = digit_count(a, first_a);
+    let n = digit_count(b, first_b);
+    if m < n {
+        return Ok((BigInt::from_i64(0), BigInt::from_words(a)));
+    }
+
+    // Algorithm D in radix 2^32 keeps products and estimates in u64. Digits 
below
+    // are least-significant first; the scalar dispatch left at least two in b.
+    let top_word = abs_word(b, first_b, (n - 1) / 2) as u64;
+    let mut top_digit = (top_word >> (((n - 1) % 2) * 32)) as u32;
+    let mut shift = 0u32;
+    // Normalize both inputs until B[0] >= base/2, bounding the quotient 
estimate error.
+    while top_digit < (1u32 << 31) {
+        top_digit <<= 1;
+        shift += 1;
+    }
+    let nq = (m - n) / 2 + 2;
+    let nr = n.div_ceil(2) + 1;
+    // The quotient words come first, then the Algorithm D scratch, in one 
buffer.
+    let capacity = nq
+        .checked_add(m + 1)
+        .and_then(|words| words.checked_add(n))
+        .ok_or_else(|| overflow("BigInt division workspace is too large"))?;
+    let mut quotient = WordsBuf::new();
+    let mut remainder = WordsBuf::new();
+    let (q, scratch) = quotient.zeroed(capacity).split_at_mut(nq);
+    let (u, v) = scratch.split_at_mut(m + 1);
+    let r = remainder.zeroed(nr);
+    let normalize = |x: &[i64], first: usize, digits: usize, out: &mut [i64]| 
-> u32 {
+        let mut carry = 0u64;
+        for i in 0..digits {
+            let word = abs_word(x, first, i / 2) as u64;
+            let part = (word >> ((i % 2) * 32)) as u32 as u64;
+            let value = (part << shift) | carry;
+            out[i] = (value as u32) as i64;
+            carry = value >> 32;
+        }
+        carry as u32
+    };
+    // The dividend also keeps its shifted high carry.
+    u[m] = i64::from(normalize(a, first_a, m, &mut u[..m]));
+    normalize(b, first_b, n, &mut v[..n]);
+
+    let mut quotient_word = 0u64;
+    for j in (0..=(m - n)).rev() {
+        // Step 1: estimate q = A[0]A[1] / B[0] with A[0] = u[j+n], B[0] = 
v[n-1];
+        // A[0] <= B[0], and equality is clamped so q stays a radix digit.
+        let (mut estimate, mut residual) = if u[j + n] == v[n - 1] {
+            (BASE - 1, u[j + n - 1] as u64 + v[n - 1] as u64)
+        } else {
+            let numerator = ((u[j + n] as u64) << 32) | u[j + n - 1] as u64;
+            (numerator / v[n - 1] as u64, numerator % v[n - 1] as u64)
+        };
+        // Step 2: refine q with B[1] until q*B[1] <= r*base + A[2]; at most 
two
+        // decrements, and r < base guards the shift.
+        while residual < BASE && estimate * v[n - 2] as u64 > (residual << 32) 
+ u[j + n - 2] as u64
+        {
+            estimate -= 1;
+            residual += v[n - 1] as u64;
+        }
+        // Step 3: subtract q * B from the window; q is correct or one too 
high.
+        let mut borrow = 0u64;
+        for i in 0..n {
+            // A radix-digit product plus the incoming borrow fits u64.
+            let product = estimate * v[i] as u64 + borrow;
+            let low = product as u32;
+            let old = u[j + i] as u32;
+            u[j + i] = i64::from(old.wrapping_sub(low));
+            borrow = (product >> 32) + u64::from(old < low);
+        }
+        let old = u[j + n] as u64;
+        // Only a borrow out of the full window means the result went negative.
+        let negative = old < borrow;
+        u[j + n] = i64::from(old.wrapping_sub(borrow) as u32);
+        if negative {
+            // Step 4: decrement q and add B back, restoring 0 <= R < divisor.
+            estimate -= 1;
+            let mut carry = 0u64;
+            for i in 0..n {
+                let sum = u[j + i] as u64 + v[i] as u64 + carry;
+                u[j + i] = i64::from(sum as u32);
+                carry = sum >> 32;
+            }
+            u[j + n] = i64::from((u[j + n] as u64 + carry) as u32);
+        }
+        // Pack descending quotient digits into complete words.
+        if j % 2 == 1 {
+            quotient_word = estimate << 32;
+        } else {
+            q[j / 2] = (quotient_word | estimate) as i64;
+        }
+    }
+    // Extract the remainder, undoing the normalization shift.
+    for i in 0..nr - 1 {
+        let digit = 2 * i;
+        let mut word = u[digit] as u64;
+        if digit + 1 < n {
+            word |= (u[digit + 1] as u64) << 32;
+        }
+        word >>= shift;
+        // Bring down bits from the next digit; shift zero never evaluates a 
shift by 64.
+        if shift != 0 && digit + 2 < n {
+            word |= (u[digit + 2] as u64) << (64 - shift);
+        }
+        r[i] = word as i64;
+    }
+    // The quotient takes the operand-sign XOR, a nonzero remainder the 
dividend's sign.
+    if is_negative(a) != is_negative(b) {
+        negate_in_place(q);
+    }
+    if is_negative(a) {
+        negate_in_place(r);
+    }
+    Ok((BigInt::from_words(q), BigInt::from_words(r)))
+}
+
+/// Quotient rounded toward zero.
+pub(super) fn trunc_div(a: &[i64], b: &[i64]) -> Result<BigInt> {
+    Ok(div_rem(a, b)?.0)
+}
+
+/// Remainder of truncating division: zero or with the dividend's sign.
+pub(super) fn trunc_mod(a: &[i64], b: &[i64]) -> Result<BigInt> {
+    if b.len() == 1 {
+        if b[0] == 0 {
+            return Err(zero_division());
+        }
+        let divisor = b[0].unsigned_abs();
+        if divisor <= u64::from(u32::MAX) {
+            // remainder < divisor < 2^32: each brought-down half fits u64 and 
the
+            // result is inline, so no quotient or allocation is needed.
+            let first = first_nonzero(a);
+            let mut remainder = 0u64;
+            for i in (0..a.len()).rev() {
+                let word = abs_word(a, first, i) as u64;
+                remainder = ((remainder << 32) | (word >> 32)) % divisor;
+                remainder = ((remainder << 32) | (word as u32 as u64)) % 
divisor;
+            }
+            let result = remainder as i64;
+            return Ok(BigInt::from_i64(if is_negative(a) {
+                -result
+            } else {
+                result
+            }));
+        }
+    }
+    Ok(div_rem(a, b)?.1)
+}
+
+/// Quotient rounded toward negative infinity.
+pub(super) fn floor_div(a: &[i64], b: &[i64]) -> Result<BigInt> {
+    let (quotient, remainder) = div_rem(a, b)?;
+    if !remainder.is_zero() && is_negative(a) != is_negative(b) {
+        // With a nonzero remainder, (q-1)*b + (r+b) preserves a while 
rounding down.
+        return Ok(sub(quotient.words(), &[1]));
+    }
+    Ok(quotient)
+}
+
+/// Remainder of floor division: zero or with the divisor's sign.
+pub(super) fn floor_mod(a: &[i64], b: &[i64]) -> Result<BigInt> {
+    let remainder = trunc_mod(a, b)?;
+    if !remainder.is_zero() && is_negative(a) != is_negative(b) {
+        // The floor remainder is r+b, which |r| < |b| keeps inline for a 
scalar divisor.
+        if let ([divisor], Some(r)) = (b, remainder.to_i64()) {
+            return Ok(BigInt::from_i64(r + divisor));
+        }
+        return Ok(add(remainder.words(), b));
+    }
+    Ok(remainder)
+}
+
+/// Convert to the nearest double, ties to even; `OverflowError` beyond the 
finite range.
+pub(super) fn to_f64(x: &[i64]) -> Result<f64> {
+    let first = first_nonzero(x);
+    let mut size = x.len();
+    while size > 0 && abs_word(x, first, size - 1) == 0 {
+        size -= 1;
+    }
+    if size == 0 {
+        return Ok(0.0);
+    }
+    let does_not_fit = || overflow("BigInt does not fit finite double");
+    if size > 16 {
+        return Err(does_not_fit());
+    }
+    let high = abs_word(x, first, size - 1) as u64;
+    let bits = (size - 1) * 64 + (64 - high.leading_zeros() as usize);
+    let shift = bits.saturating_sub(53);
+    let bit = |i: usize| ((abs_word(x, first, i / 64) as u64) >> (i % 64)) & 1;
+    let mut significand = 0u64;
+    for i in (shift..bits).rev() {
+        significand = (significand << 1) | bit(i);
+    }
+    if shift > 0 {
+        let sticky = (0..shift - 1).any(|i| bit(i) != 0);
+        if bit(shift - 1) == 1 && (sticky || significand & 1 == 1) {
+            significand += 1;
+        }
+    }
+    if bits == 1024 && significand == 1 << 53 {
+        return Err(does_not_fit());
+    }
+    let result = significand as f64 * 2f64.powi(shift as i32);
+    Ok(if is_negative(x) { -result } else { result })
+}
+
+/// Multiply magnitude words by `factor` and add `addend`, growing as needed.
+pub(super) fn mul_add_small(magnitude: &mut Vec<u64>, factor: u64, addend: 
u64) {
+    let mut carry = u128::from(addend);
+    for word in magnitude.iter_mut() {
+        let product = u128::from(*word) * u128::from(factor) + carry;
+        *word = product as u64;
+        carry = product >> 64;
+    }
+    if carry != 0 {
+        magnitude.push(carry as u64);
+    }
+}
diff --git a/rust/tvm-ffi/src/big_int/mod.rs b/rust/tvm-ffi/src/big_int/mod.rs
new file mode 100644
index 00000000..07857937
--- /dev/null
+++ b/rust/tvm-ffi/src/big_int/mod.rs
@@ -0,0 +1,862 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+//! Integers with inline `i64` storage and arbitrary-precision arithmetic.
+mod int_ops;
+
+use crate::derive::Object;
+use crate::error::{Error, Result, OVERFLOW_ERROR, VALUE_ERROR};
+use crate::object::{unsafe_, Object, ObjectArc, ObjectCoreWithExtraItems};
+use crate::type_traits::AnyCompatible;
+use std::alloc::Layout;
+use std::cmp::Ordering;
+use std::ffi::c_void;
+use std::fmt::{Debug, Display, Write as _};
+use std::hash::{Hash, Hasher};
+use std::ops::{
+    Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, 
BitXorAssign, Div, DivAssign,
+    Mul, MulAssign, Neg, Not, Rem, RemAssign, Shl, ShlAssign, Shr, ShrAssign, 
Sub, SubAssign,
+};
+use std::str::FromStr;
+use std::sync::atomic::AtomicU64;
+use 
tvm_ffi_sys::TVMFFIObjectDeleterFlagBitMask::kTVMFFIObjectDeleterFlagBitMaskWeak;
+use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
+use tvm_ffi_sys::{TVMFFIAny, TVMFFIAnyDataUnion, TVMFFIObject, 
COMBINED_REF_COUNT_BOTH_ONE};
+
+/// ABI stable arbitrary-precision signed integer for ffi.
+///
+/// Mirrors C++ `tvm::ffi::BigInt`: a value that fits `i64` is stored inline as
+/// `kTVMFFIInt`, and any wider value owns a `kTVMFFIBigInt` object holding its
+/// minimal two's-complement `i64` words, least-significant word first. Every
+/// operation returns that canonical form, so word-wise comparison decides 
equality.
+///
+/// Arithmetic is unbounded and signed. `/` and `%` truncate toward zero;
+/// [`floor_div`](Self::floor_div) and [`floor_mod`](Self::floor_mod) round 
down.
+/// Bitwise operations sign-extend infinitely, and negative shift counts are 
errors.
+/// Like the primitive operators, `/`, `%`, `<<` and `>>` panic on those 
errors;
+/// the `try_*` methods return them instead, as ffi callbacks need since a 
panic
+/// cannot unwind across them.
+#[repr(C)]
+pub struct BigInt {
+    data: TVMFFIAny,
+}
+
+// BigIntObj for heap-allocated integers: the word count, then the canonical
+// two's-complement words as trailing items. Mirrors C++ `details::BigIntObj`.
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "ffi.BigInt"]
+#[type_index(TypeIndex::kTVMFFIBigInt)]
+#[type_final]
+struct BigIntObj {
+    object: Object,
+    size: usize,
+}
+
+unsafe impl ObjectCoreWithExtraItems for BigIntObj {
+    type ExtraItem = i64;
+    #[inline]
+    /// Get the count of extra items (the logical word count)
+    fn extra_items_count(this: &Self) -> usize {
+        this.size
+    }
+}
+
+// `BigInt::try_build` allocates from the C allocator like C++
+// `make_inplace_array_object`, so it can prune the logical size in place (C++
+// `ShrinkSize`): `free` needs no layout, unlike Rust's global allocator.
+extern "C" {
+    fn malloc(size: usize) -> *mut c_void;
+    fn free(ptr: *mut c_void);
+}
+
+/// Release an object from `BigInt::try_build` like C++ 
`ArrayHandler::Deleter_`;
+/// the words need no destructor.
+unsafe extern "C" fn delete_obj(ptr: *mut c_void, flags: i32) {
+    if flags & kTVMFFIObjectDeleterFlagBitMaskWeak as i32 != 0 {
+        free(ptr);
+    }
+}
+
+/// Layout of an object with `len` words; `None` beyond `isize::MAX` bytes.
+fn obj_layout(len: usize) -> Option<Layout> {
+    let words = Layout::array::<i64>(len).ok()?;
+    Some(Layout::new::<BigIntObj>().extend(words).ok()?.0)
+}
+
+impl BigInt {
+    /// Create the integer zero.
+    pub fn new() -> Self {
+        Self::from_i64(0)
+    }
+
+    #[inline]
+    fn from_i64(value: i64) -> Self {
+        Self {
+            data: TVMFFIAny {
+                type_index: TypeIndex::kTVMFFIInt as i32,
+                small_str_len: 0,
+                data_union: TVMFFIAnyDataUnion { v_int64: value },
+            },
+        }
+    }
+
+    /// Take ownership of a canonical multiword object without an extra 
reference.
+    #[inline]
+    fn from_obj(obj: *mut BigIntObj) -> Self {
+        Self {
+            data: TVMFFIAny {
+                type_index: TypeIndex::kTVMFFIBigInt as i32,
+                small_str_len: 0,
+                data_union: TVMFFIAnyDataUnion {
+                    v_obj: obj.cast::<TVMFFIObject>(),
+                },
+            },
+        }
+    }
+
+    /// Create from two's-complement `i64` words, least-significant word first.
+    ///
+    /// Redundant sign-extension words are pruned and a value that fits `i64`
+    /// is stored inline; an empty slice is zero.
+    #[inline]
+    pub fn from_words(words: &[i64]) -> Self {
+        let size = int_ops::normalized_len(words);
+        if size <= 1 {
+            return Self::from_i64(words.first().copied().unwrap_or(0));
+        }
+        // Allocated at its final length, so the crate's generic extra-items 
deleter fits.
+        unsafe {
+            let mut obj = ObjectArc::new_with_extra_items(BigIntObj {
+                object: Object::new(),
+                size,
+            });
+            BigIntObj::extra_items_mut(&mut 
obj).copy_from_slice(&words[..size]);
+            Self::from_obj(ObjectArc::into_raw(obj) as *mut BigIntObj)
+        }
+    }
+
+    /// Build from `len` words, word `i` being `word(i)` called in increasing 
order,
+    /// written straight into a fresh object that is then pruned in place like 
C++
+    /// `Normalize`; `None` when the object cannot be allocated.
+    #[inline]
+    fn try_build(len: usize, mut word: impl FnMut(usize) -> i64) -> 
Option<Self> {
+        let layout = obj_layout(len)?;
+        unsafe {
+            let obj = malloc(layout.size()).cast::<BigIntObj>();
+            if obj.is_null() {
+                return None;
+            }
+            let data = obj.add(1).cast::<i64>();
+            for i in 0..len {
+                data.add(i).write(word(i));
+            }
+            let size = 
int_ops::normalized_len(std::slice::from_raw_parts(data, len));
+            if size <= 1 {
+                let value = if size == 1 { *data } else { 0 };
+                free(obj.cast());
+                return Some(Self::from_i64(value));
+            }
+            obj.cast::<TVMFFIObject>().write(TVMFFIObject {
+                combined_ref_count: 
AtomicU64::new(COMBINED_REF_COUNT_BOTH_ONE),
+                type_index: TypeIndex::kTVMFFIBigInt as i32,
+                __padding: 0,
+                deleter: Some(delete_obj),
+            });
+            std::ptr::addr_of_mut!((*obj).size).write(size);
+            Some(Self::from_obj(obj))
+        }
+    }
+
+    /// Like `try_build`, but like `Vec`, exhaustion aborts.
+    #[inline]
+    fn build(len: usize, word: impl FnMut(usize) -> i64) -> Self {
+        Self::try_build(len, word).unwrap_or_else(|| {
+            
std::alloc::handle_alloc_error(obj_layout(len).unwrap_or(Layout::new::<BigIntObj>()))
+        })
+    }
+
+    /// Borrow the canonical two's-complement words, least-significant word 
first.
+    ///
+    /// The slice is never empty and its last word carries the sign.
+    pub fn words(&self) -> &[i64] {
+        unsafe {
+            if self.data.type_index == TypeIndex::kTVMFFIInt as i32 {
+                std::slice::from_ref(&self.data.data_union.v_int64)
+            } else {
+                let obj: &BigIntObj = &*(self.data.data_union.v_obj as *const 
BigIntObj);
+                BigIntObj::extra_items(obj)
+            }
+        }
+    }
+
+    /// Whether the value is negative.
+    pub fn is_negative(&self) -> bool {
+        int_ops::is_negative(self.words())
+    }
+
+    /// Whether the value is zero.
+    #[inline]
+    pub fn is_zero(&self) -> bool {
+        self.to_i64() == Some(0)
+    }
+
+    /// Return the value when it fits `i64`.
+    #[inline]
+    pub fn to_i64(&self) -> Option<i64> {
+        if self.data.type_index == TypeIndex::kTVMFFIInt as i32 {
+            Some(unsafe { self.data.data_union.v_int64 })
+        } else {
+            None
+        }
+    }
+
+    /// Return the value when it fits `u64`.
+    pub fn to_u64(&self) -> Option<u64> {
+        match *self.words() {
+            [w0] => u64::try_from(w0).ok(),
+            [w0, 0] => Some(w0 as u64),
+            _ => None,
+        }
+    }
+
+    /// Return the value when it fits `i128`.
+    pub fn to_i128(&self) -> Option<i128> {
+        match *self.words() {
+            [w0] => Some(i128::from(w0)),
+            [w0, w1] => Some((i128::from(w1) << 64) | i128::from(w0 as u64)),
+            _ => None,
+        }
+    }
+
+    /// Return the value when it fits `u128`.
+    pub fn to_u128(&self) -> Option<u128> {
+        let combine = |w0: i64, w1: i64| (u128::from(w1 as u64) << 64) | 
u128::from(w0 as u64);
+        match *self.words() {
+            [w0] => u128::try_from(w0).ok(),
+            [w0, w1] if w1 >= 0 => Some(combine(w0, w1)),
+            [w0, w1, 0] => Some(combine(w0, w1)),
+            _ => None,
+        }
+    }
+
+    /// Convert to the nearest finite double, rounding ties to even.
+    ///
+    /// Raises `OverflowError` when the value rounds beyond the finite range.
+    pub fn to_f64(&self) -> Result<f64> {
+        match self.to_i64() {
+            Some(value) => Ok(value as f64),
+            None => int_ops::to_f64(self.words()),
+        }
+    }
+
+    /// Quotient rounded toward zero; raises `ZeroDivisionError`.
+    pub fn try_div(&self, rhs: &BigInt) -> Result<BigInt> {
+        div_impl(self, rhs)
+    }
+
+    /// Remainder of truncating division, zero or with the dividend's sign.
+    pub fn try_rem(&self, rhs: &BigInt) -> Result<BigInt> {
+        rem_impl(self, rhs)
+    }
+
+    /// Truncating quotient and remainder in one pass; raises 
`ZeroDivisionError`.
+    pub fn div_rem(&self, rhs: &BigInt) -> Result<(BigInt, BigInt)> {
+        if let (Some(x), Some(y)) = (self.to_i64(), rhs.to_i64()) {
+            if let Some(q) = int_ops::trunc_div_i64(x, y)? {
+                return Ok((Self::from_i64(q), 
Self::from_i64(x.wrapping_rem(y))));
+            }
+        }
+        int_ops::div_rem(self.words(), rhs.words())
+    }
+
+    /// Quotient rounded toward negative infinity; raises `ZeroDivisionError`.
+    pub fn floor_div(&self, rhs: &BigInt) -> Result<BigInt> {
+        floor_div_impl(self, rhs)
+    }
+
+    /// Remainder of floor division, zero or with the divisor's sign.
+    pub fn floor_mod(&self, rhs: &BigInt) -> Result<BigInt> {
+        floor_mod_impl(self, rhs)
+    }
+
+    /// Left shift; negative counts raise `ValueError`, excessive growth 
`OverflowError`.
+    pub fn try_shl<C: Into<BigInt>>(&self, count: C) -> Result<BigInt> {
+        shl_impl(self, &count.into())
+    }
+
+    /// Sign-extending right shift; negative counts raise `ValueError`.
+    pub fn try_shr<C: Into<BigInt>>(&self, count: C) -> Result<BigInt> {
+        shr_impl(self, &count.into())
+    }
+}
+
+impl Default for BigInt {
+    #[inline]
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Macro to implement From for integer types that always fit i64
+macro_rules! impl_big_int_from_int {
+    ($($int_type:ty),* $(,)?) => {
+        $(
+            impl From<$int_type> for BigInt {
+                #[inline]
+                fn from(value: $int_type) -> Self {
+                    Self::from_i64(value as i64)
+                }
+            }
+        )*
+    };
+}
+
+impl_big_int_from_int!(i8, i16, i32, i64, isize, u8, u16, u32);
+
+impl From<bool> for BigInt {
+    #[inline]
+    fn from(value: bool) -> Self {
+        Self::from_i64(i64::from(value))
+    }
+}
+
+impl From<u64> for BigInt {
+    fn from(value: u64) -> Self {
+        match i64::try_from(value) {
+            Ok(value) => Self::from_i64(value),
+            Err(_) => Self::from_words(&[value as i64, 0]),
+        }
+    }
+}
+
+impl From<usize> for BigInt {
+    #[inline]
+    fn from(value: usize) -> Self {
+        Self::from(value as u64)
+    }
+}
+
+impl From<i128> for BigInt {
+    fn from(value: i128) -> Self {
+        match i64::try_from(value) {
+            Ok(value) => Self::from_i64(value),
+            // The arithmetic shift keeps the sign in the high word.
+            Err(_) => Self::from_words(&[value as i64, (value >> 64) as i64]),
+        }
+    }
+}
+
+impl From<u128> for BigInt {
+    fn from(value: u128) -> Self {
+        match i64::try_from(value) {
+            Ok(value) => Self::from_i64(value),
+            // The zero guard keeps the value positive; normalization prunes 
it when redundant.
+            Err(_) => Self::from_words(&[value as i64, (value >> 64) as i64, 
0]),
+        }
+    }
+}
+
+impl TryFrom<f64> for BigInt {
+    type Error = Error;
+
+    /// Convert a finite double by truncating toward zero.
+    ///
+    /// NaN raises `ValueError` and infinity raises `OverflowError`.
+    fn try_from(value: f64) -> Result<Self> {
+        // The upper bound is exclusive: i64::MAX rounds up to 2^63 as a 
double.
+        const LOWER: f64 = i64::MIN as f64;
+        if (LOWER..-LOWER).contains(&value) {
+            return Ok(Self::from_i64(value as i64));
+        }
+        if value.is_nan() {
+            return Err(Error::new(VALUE_ERROR, "Cannot convert NaN to BigInt", 
""));
+        }
+        if value.is_infinite() {
+            return Err(Error::new(
+                OVERFLOW_ERROR,
+                "Cannot convert infinity to BigInt",
+                "",
+            ));
+        }
+        // |value| >= 2^63 is an integer: shift the 53-bit significand to its 
binary exponent.
+        let bits = value.to_bits();
+        let exponent = ((bits >> 52) & 0x7ff) as i64 - 1075;
+        let significand = ((bits & ((1u64 << 52) - 1)) | (1u64 << 52)) as i64;
+        let magnitude = Self::from_i64(significand) << exponent;
+        Ok(if value < 0.0 { -magnitude } else { magnitude })
+    }
+}
+
+impl FromStr for BigInt {
+    type Err = Error;
+
+    /// Parse an optionally signed decimal integer; anything else raises 
`ValueError`.
+    fn from_str(text: &str) -> Result<Self> {
+        if let Ok(value) = text.parse::<i64>() {
+            return Ok(Self::from_i64(value));
+        }
+        let (negative, digits) = match text.as_bytes().first() {
+            Some(b'-') => (true, &text[1..]),
+            Some(b'+') => (false, &text[1..]),
+            _ => (false, text),
+        };
+        if digits.is_empty() || !digits.bytes().all(|byte| 
byte.is_ascii_digit()) {
+            return Err(Error::new(
+                VALUE_ERROR,
+                "Invalid decimal string for BigInt",
+                "",
+            ));
+        }
+        // Accumulate 19-digit chunks: magnitude = magnitude * 10^len + chunk.
+        let mut magnitude = Vec::new();
+        for chunk in digits.as_bytes().chunks(19) {
+            let value = chunk
+                .iter()
+                .fold(0u64, |acc, &digit| acc * 10 + u64::from(digit - b'0'));
+            int_ops::mul_add_small(&mut magnitude, 10u64.pow(chunk.len() as 
u32), value);
+        }
+        let mut words: Vec<i64> = magnitude.into_iter().map(|word| word as 
i64).collect();
+        words.push(0);
+        if negative {
+            int_ops::negate_in_place(&mut words);
+        }
+        Ok(Self::from_words(&words))
+    }
+}
+
+impl Clone for BigInt {
+    #[inline]
+    fn clone(&self) -> Self {
+        if self.data.type_index == TypeIndex::kTVMFFIBigInt as i32 {
+            unsafe { unsafe_::inc_ref(self.data.data_union.v_obj) }
+        }
+        Self { data: self.data }
+    }
+}
+
+impl Drop for BigInt {
+    #[inline]
+    fn drop(&mut self) {
+        if self.data.type_index == TypeIndex::kTVMFFIBigInt as i32 {
+            unsafe { unsafe_::dec_ref(self.data.data_union.v_obj) }
+        }
+    }
+}
+
+impl PartialEq for BigInt {
+    /// Canonical words make equal values word-for-word identical.
+    #[inline]
+    fn eq(&self, other: &Self) -> bool {
+        self.words() == other.words()
+    }
+}
+
+impl Eq for BigInt {}
+
+impl PartialOrd for BigInt {
+    #[inline]
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for BigInt {
+    fn cmp(&self, other: &Self) -> Ordering {
+        let (a, b) = (self.words(), other.words());
+        let (negative_a, negative_b) = (int_ops::is_negative(a), 
int_ops::is_negative(b));
+        if negative_a != negative_b {
+            return if negative_a {
+                Ordering::Less
+            } else {
+                Ordering::Greater
+            };
+        }
+        // With equal signs, sign-extended words compare as unsigned from the 
top.
+        let extension = int_ops::extension(a);
+        (0..a.len().max(b.len()))
+            .rev()
+            .map(|i| int_ops::word_at(a, i, 
extension).cmp(&int_ops::word_at(b, i, extension)))
+            .find(|ordering| ordering.is_ne())
+            .unwrap_or(Ordering::Equal)
+    }
+}
+
+impl Hash for BigInt {
+    #[inline]
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.words().hash(state);
+    }
+}
+
+impl Display for BigInt {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if let Some(value) = self.to_i64() {
+            return Display::fmt(&value, f);
+        }
+        let words = self.words();
+        let negative = words[words.len() - 1] < 0;
+        let mut magnitude = words.to_vec();
+        if negative {
+            int_ops::negate_in_place(&mut magnitude);
+        }
+        // Peel base-10^19 chunks, least significant first, by long division.
+        const CHUNK: u128 = 10_000_000_000_000_000_000;
+        let mut chunks = Vec::new();
+        while let Some(top) = magnitude.iter().rposition(|&word| word != 0) {
+            magnitude.truncate(top + 1);
+            let mut remainder = 0u128;
+            for word in magnitude.iter_mut().rev() {
+                let current = (remainder << 64) | u128::from(*word as u64);
+                *word = (current / CHUNK) as i64;
+                remainder = current % CHUNK;
+            }
+            chunks.push(remainder as u64);
+        }
+        let mut digits = String::new();
+        let mut chunks = chunks.iter().rev();
+        // A heap value is never zero, so the leading chunk always exists.
+        write!(digits, "{}", chunks.next().unwrap_or(&0))?;
+        for chunk in chunks {
+            write!(digits, "{chunk:019}")?;
+        }
+        f.pad_integral(!negative, "", &digits)
+    }
+}
+
+impl Debug for BigInt {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ffi.BigInt")
+            .field("data", &format_args!("{self}"))
+            .finish()
+    }
+}
+
+// ---- Arithmetic: an inline fast path, then the word fallbacks.
+#[inline]
+fn add_impl(a: &BigInt, b: &BigInt) -> BigInt {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = x.checked_add(y) {
+            return BigInt::from_i64(result);
+        }
+    }
+    int_ops::add(a.words(), b.words())
+}
+
+#[inline]
+fn sub_impl(a: &BigInt, b: &BigInt) -> BigInt {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = x.checked_sub(y) {
+            return BigInt::from_i64(result);
+        }
+    }
+    int_ops::sub(a.words(), b.words())
+}
+
+#[inline]
+fn mul_impl(a: &BigInt, b: &BigInt) -> BigInt {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = x.checked_mul(y) {
+            return BigInt::from_i64(result);
+        }
+    }
+    int_ops::mul(a.words(), b.words())
+}
+
+#[inline]
+fn div_impl(a: &BigInt, b: &BigInt) -> Result<BigInt> {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = int_ops::trunc_div_i64(x, y)? {
+            return Ok(BigInt::from_i64(result));
+        }
+    }
+    int_ops::trunc_div(a.words(), b.words())
+}
+
+#[inline]
+fn rem_impl(a: &BigInt, b: &BigInt) -> Result<BigInt> {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = int_ops::trunc_mod_i64(x, y)? {
+            return Ok(BigInt::from_i64(result));
+        }
+    }
+    int_ops::trunc_mod(a.words(), b.words())
+}
+
+#[inline]
+fn floor_div_impl(a: &BigInt, b: &BigInt) -> Result<BigInt> {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = int_ops::floor_div_i64(x, y)? {
+            return Ok(BigInt::from_i64(result));
+        }
+    }
+    int_ops::floor_div(a.words(), b.words())
+}
+
+#[inline]
+fn floor_mod_impl(a: &BigInt, b: &BigInt) -> Result<BigInt> {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = int_ops::floor_mod_i64(x, y)? {
+            return Ok(BigInt::from_i64(result));
+        }
+    }
+    int_ops::floor_mod(a.words(), b.words())
+}
+
+#[inline]
+fn shl_impl(a: &BigInt, b: &BigInt) -> Result<BigInt> {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        if let Some(result) = int_ops::shl_i64(x, y)? {
+            return Ok(BigInt::from_i64(result));
+        }
+    }
+    int_ops::shl(a.words(), b.words())
+}
+
+#[inline]
+fn shr_impl(a: &BigInt, b: &BigInt) -> Result<BigInt> {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        return Ok(BigInt::from_i64(int_ops::shr_i64(x, y)?));
+    }
+    int_ops::shr(a.words(), b.words())
+}
+
+#[inline]
+fn and_impl(a: &BigInt, b: &BigInt) -> BigInt {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        return BigInt::from_i64(x & y);
+    }
+    int_ops::and(a.words(), b.words())
+}
+
+#[inline]
+fn or_impl(a: &BigInt, b: &BigInt) -> BigInt {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        return BigInt::from_i64(x | y);
+    }
+    int_ops::or(a.words(), b.words())
+}
+
+#[inline]
+fn xor_impl(a: &BigInt, b: &BigInt) -> BigInt {
+    if let (Some(x), Some(y)) = (a.to_i64(), b.to_i64()) {
+        return BigInt::from_i64(x ^ y);
+    }
+    int_ops::xor(a.words(), b.words())
+}
+
+#[inline]
+fn neg_impl(a: &BigInt) -> BigInt {
+    if let Some(negated) = a.to_i64().and_then(i64::checked_neg) {
+        return BigInt::from_i64(negated);
+    }
+    int_ops::negate(a.words())
+}
+
+#[inline]
+fn not_impl(a: &BigInt) -> BigInt {
+    if let Some(x) = a.to_i64() {
+        return BigInt::from_i64(!x);
+    }
+    int_ops::not(a.words())
+}
+
+/// The operator form of a fallible operation panics like the primitive one 
would.
+macro_rules! define_op_or_panic {
+    ($($name:ident => $imp:ident),* $(,)?) => {
+        $(
+            #[inline]
+            fn $name(a: &BigInt, b: &BigInt) -> BigInt {
+                $imp(a, b).unwrap_or_else(|error| panic!("{}", 
error.message()))
+            }
+        )*
+    };
+}
+
+define_op_or_panic!(
+    div_or_panic => div_impl,
+    rem_or_panic => rem_impl,
+    shl_or_panic => shl_impl,
+    shr_or_panic => shr_impl,
+);
+
+/// Forward an operator to its implementation for owned and borrowed operands,
+/// with `i64` accepted on either side as C++ does.
+macro_rules! forward_binop {
+    ($Trait:ident::$method:ident, $Assign:ident::$assign:ident, $imp:ident) => 
{
+        impl $Trait<&BigInt> for &BigInt {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: &BigInt) -> BigInt {
+                $imp(self, rhs)
+            }
+        }
+        impl $Trait<BigInt> for &BigInt {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: BigInt) -> BigInt {
+                $imp(self, &rhs)
+            }
+        }
+        impl $Trait<&BigInt> for BigInt {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: &BigInt) -> BigInt {
+                $imp(&self, rhs)
+            }
+        }
+        impl $Trait<BigInt> for BigInt {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: BigInt) -> BigInt {
+                $imp(&self, &rhs)
+            }
+        }
+        impl $Trait<i64> for &BigInt {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: i64) -> BigInt {
+                $imp(self, &BigInt::from_i64(rhs))
+            }
+        }
+        impl $Trait<i64> for BigInt {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: i64) -> BigInt {
+                $imp(&self, &BigInt::from_i64(rhs))
+            }
+        }
+        impl $Trait<&BigInt> for i64 {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: &BigInt) -> BigInt {
+                $imp(&BigInt::from_i64(self), rhs)
+            }
+        }
+        impl $Trait<BigInt> for i64 {
+            type Output = BigInt;
+            #[inline]
+            fn $method(self, rhs: BigInt) -> BigInt {
+                $imp(&BigInt::from_i64(self), &rhs)
+            }
+        }
+        impl $Assign<&BigInt> for BigInt {
+            #[inline]
+            fn $assign(&mut self, rhs: &BigInt) {
+                *self = $imp(self, rhs);
+            }
+        }
+        impl $Assign<BigInt> for BigInt {
+            #[inline]
+            fn $assign(&mut self, rhs: BigInt) {
+                *self = $imp(self, &rhs);
+            }
+        }
+        impl $Assign<i64> for BigInt {
+            #[inline]
+            fn $assign(&mut self, rhs: i64) {
+                *self = $imp(self, &BigInt::from_i64(rhs));
+            }
+        }
+    };
+}
+
+forward_binop!(Add::add, AddAssign::add_assign, add_impl);
+forward_binop!(Sub::sub, SubAssign::sub_assign, sub_impl);
+forward_binop!(Mul::mul, MulAssign::mul_assign, mul_impl);
+forward_binop!(Div::div, DivAssign::div_assign, div_or_panic);
+forward_binop!(Rem::rem, RemAssign::rem_assign, rem_or_panic);
+forward_binop!(BitAnd::bitand, BitAndAssign::bitand_assign, and_impl);
+forward_binop!(BitOr::bitor, BitOrAssign::bitor_assign, or_impl);
+forward_binop!(BitXor::bitxor, BitXorAssign::bitxor_assign, xor_impl);
+forward_binop!(Shl::shl, ShlAssign::shl_assign, shl_or_panic);
+forward_binop!(Shr::shr, ShrAssign::shr_assign, shr_or_panic);
+
+impl Neg for &BigInt {
+    type Output = BigInt;
+    #[inline]
+    fn neg(self) -> BigInt {
+        neg_impl(self)
+    }
+}
+
+impl Neg for BigInt {
+    type Output = BigInt;
+    #[inline]
+    fn neg(self) -> BigInt {
+        neg_impl(&self)
+    }
+}
+
+impl Not for &BigInt {
+    type Output = BigInt;
+    #[inline]
+    fn not(self) -> BigInt {
+        not_impl(self)
+    }
+}
+
+impl Not for BigInt {
+    type Output = BigInt;
+    #[inline]
+    fn not(self) -> BigInt {
+        not_impl(&self)
+    }
+}
+
+// ---- AnyCompatible.
+unsafe impl AnyCompatible for BigInt {
+    fn type_str() -> std::string::String {
+        "BigInt".to_string()
+    }
+
+    unsafe fn copy_to_any_view(this: &Self, data: &mut TVMFFIAny) {
+        *data = this.data;
+    }
+
+    unsafe fn move_to_any(src: Self, data: &mut TVMFFIAny) {
+        *data = src.data;
+        std::mem::forget(src);
+    }
+
+    unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
+        data.type_index == TypeIndex::kTVMFFIInt as i32
+            || data.type_index == TypeIndex::kTVMFFIBigInt as i32
+    }
+
+    unsafe fn copy_from_any_view_after_check(data: &TVMFFIAny) -> Self {
+        if data.type_index == TypeIndex::kTVMFFIBigInt as i32 {
+            unsafe { unsafe_::inc_ref(data.data_union.v_obj) }
+        }
+        Self { data: *data }
+    }
+
+    unsafe fn move_from_any_after_check(data: &mut TVMFFIAny) -> Self {
+        Self { data: *data }
+    }
+
+    unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> 
std::result::Result<Self, ()> {
+        if Self::check_any_strict(data) {
+            Ok(Self::copy_from_any_view_after_check(data))
+        } else if data.type_index == TypeIndex::kTVMFFIBool as i32 {
+            Ok(Self::from_i64(data.data_union.v_int64))
+        } else {
+            Err(())
+        }
+    }
+}
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index f540324a..2823ba71 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -49,6 +49,7 @@ pub const KEY_ERROR: ErrorKind = ErrorKind("KeyError");
 pub const INDEX_ERROR: ErrorKind = ErrorKind("IndexError");
 pub const INTERNAL_ERROR: ErrorKind = ErrorKind("InternalError");
 pub const OVERFLOW_ERROR: ErrorKind = ErrorKind("OverflowError");
+pub const ZERO_DIVISION_ERROR: ErrorKind = ErrorKind("ZeroDivisionError");
 
 /// error object
 #[repr(C)]
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index f90c88b2..5d6425c0 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -48,7 +48,7 @@ pub use crate::dtype::DLDataTypeExt;
 pub use crate::error::{Error, ErrorKind, Result};
 pub use crate::error::{
     ATTRIBUTE_ERROR, INDEX_ERROR, INTERNAL_ERROR, KEY_ERROR, OVERFLOW_ERROR, 
RUNTIME_ERROR,
-    TYPE_ERROR, VALUE_ERROR,
+    TYPE_ERROR, VALUE_ERROR, ZERO_DIVISION_ERROR,
 };
 pub use crate::extra::module::Module;
 pub use crate::extra::structural_mutate::{
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index 8b19b5f0..f05d1a09 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -418,6 +418,14 @@ pub mod unsafe_ {
         }
     }
 
+    /// Layout of a `T` followed by `count` extra items, kept at least one 
word past the
+    /// header: once a weak reference outlives the data, the deleter stores 
`count` there.
+    pub(crate) fn extra_items_layout<T, U>(count: usize) -> std::alloc::Layout 
{
+        let size = std::mem::size_of::<T>() + count * std::mem::size_of::<U>();
+        let min_size = std::mem::size_of::<TVMFFIObject>() + 
std::mem::size_of::<u64>();
+        std::alloc::Layout::from_size_align(size.max(min_size), 
std::mem::align_of::<T>()).unwrap()
+    }
+
     pub(crate) unsafe extern "C" fn object_deleter_for_new_with_extra_items<T, 
U>(
         ptr: *mut c_void,
         flags: i32,
@@ -428,27 +436,25 @@ pub mod unsafe_ {
         if flags == kTVMFFIObjectDeleterFlagBitMaskBoth as i32 {
             let extra_items_count = T::extra_items_count(&(*obj));
             std::ptr::drop_in_place(obj);
-            let layout = std::alloc::Layout::from_size_align(
-                std::mem::size_of::<T>() + extra_items_count * 
std::mem::size_of::<U>(),
-                std::mem::align_of::<T>(),
-            )
-            .unwrap();
-            std::alloc::dealloc(ptr as *mut u8, layout);
+            std::alloc::dealloc(
+                ptr as *mut u8,
+                extra_items_layout::<T, U>(extra_items_count),
+            );
         } else {
-            assert_eq!(std::mem::size_of::<T>() % std::mem::size_of::<u64>(), 
0);
+            // Weak references keep using the header, so the weak phase finds 
the item
+            // count in the first word after it.
+            let count_slot = (ptr as *mut 
u8).add(std::mem::size_of::<TVMFFIObject>()) as *mut u64;
             if flags & kTVMFFIObjectDeleterFlagBitMaskStrong as i32 != 0 {
                 let extra_items_count = T::extra_items_count(&(*obj));
                 std::ptr::drop_in_place(obj);
-                std::ptr::write(obj as *mut u64, extra_items_count as u64);
+                std::ptr::write(count_slot, extra_items_count as u64);
             }
             if flags & kTVMFFIObjectDeleterFlagBitMaskWeak as i32 != 0 {
-                let extra_items_count = std::ptr::read(obj as *mut u64) as 
usize;
-                let layout = std::alloc::Layout::from_size_align(
-                    std::mem::size_of::<T>() + extra_items_count * 
std::mem::size_of::<U>(),
-                    std::mem::align_of::<T>(),
-                )
-                .unwrap();
-                std::alloc::dealloc(ptr as *mut u8, layout);
+                let extra_items_count = std::ptr::read(count_slot) as usize;
+                std::alloc::dealloc(
+                    ptr as *mut u8,
+                    extra_items_layout::<T, U>(extra_items_count),
+                );
             }
         }
     }
@@ -520,11 +526,7 @@ impl<T: ObjectCore> ObjectArc<T> {
             assert_eq!(std::mem::align_of::<T>() % std::mem::align_of::<U>(), 
0);
             assert_eq!(std::mem::size_of::<T>() % std::mem::align_of::<U>(), 
0);
             let extra_items_count = T::extra_items_count(&data);
-            let layout = std::alloc::Layout::from_size_align(
-                std::mem::size_of::<T>() + extra_items_count * 
std::mem::size_of::<U>(),
-                std::mem::align_of::<T>(),
-            )
-            .unwrap();
+            let layout = unsafe_::extra_items_layout::<T, 
U>(extra_items_count);
             let raw_data_ptr = std::alloc::alloc(layout);
             if raw_data_ptr.is_null() {
                 std::alloc::handle_alloc_error(layout);
diff --git a/rust/tvm-ffi/tests/test_big_int.rs 
b/rust/tvm-ffi/tests/test_big_int.rs
index a64baf38..24765e04 100644
--- a/rust/tvm-ffi/tests/test_big_int.rs
+++ b/rust/tvm-ffi/tests/test_big_int.rs
@@ -60,6 +60,8 @@ fn test_big_int_inline_representation() {
         assert_eq!(value.to_i64(), Some(0));
     }
     let inline = [
+        BigInt::from(false),
+        BigInt::from(true),
         BigInt::from(-1i8),
         BigInt::from(i16::MIN),
         BigInt::from(u32::MAX),
@@ -70,6 +72,8 @@ fn test_big_int_inline_representation() {
         BigInt::from(u128::from(u64::MAX >> 1)),
     ];
     let expected = [
+        0,
+        1,
         -1,
         i64::from(i16::MIN),
         i64::from(u32::MAX),
@@ -88,49 +92,11 @@ fn test_big_int_inline_representation() {
 }
 
 #[test]
-fn test_big_int_from_bool() {
-    for (input, expected) in [(false, 0i64), (true, 1)] {
-        let value = BigInt::from(input);
-        assert_eq!(type_index_of(&value), TypeIndex::kTVMFFIInt as i32);
-        assert_eq!(value.to_i64(), Some(expected));
-    }
-}
-
-/// Rebuild a double from canonical words; exact when the magnitude has at 
most 53 significant bits.
-fn words_as_f64(value: &BigInt) -> f64 {
-    let negative = value.is_negative();
-    let mut words: Vec<u64> = value
-        .words()
-        .iter()
-        .map(|&w| if negative { !(w as u64) } else { w as u64 })
-        .collect();
-    if negative {
-        for word in words.iter_mut() {
-            let (sum, overflow) = word.overflowing_add(1);
-            *word = sum;
-            if !overflow {
-                break;
-            }
-        }
-    }
-    let magnitude: f64 = words
-        .iter()
-        .enumerate()
-        .filter(|(_, &word)| word != 0)
-        .map(|(i, &word)| word as f64 * 2f64.powi(64 * i as i32))
-        .sum();
-    if negative {
-        -magnitude
-    } else {
-        magnitude
-    }
-}
-
-#[test]
-fn test_big_int_from_f64() {
+fn test_big_int_f64_conversions() {
     let cases = [
         (0.0, BigInt::from(0i64)),
         (-0.0, BigInt::from(0i64)),
+        (f64::MIN_POSITIVE, BigInt::from(0i64)),
         (1.9, BigInt::from(1i64)),
         (-1.9, BigInt::from(-1i64)),
         (-0.999, BigInt::from(0i64)),
@@ -163,7 +129,7 @@ fn test_big_int_from_f64() {
         let value = BigInt::try_from(input).unwrap();
         assert_eq!(type_index_of(&value), TypeIndex::kTVMFFIBigInt as i32);
         assert_eq!(value.is_negative(), input < 0.0);
-        assert_eq!(words_as_f64(&value), input, "{input}");
+        assert_eq!(value.to_f64().unwrap(), input, "{input}");
     }
     assert_eq!(
         BigInt::try_from(3.0 * 2f64.powi(191)).unwrap().words(),
@@ -177,6 +143,23 @@ fn test_big_int_from_f64() {
         assert_eq!(error.kind(), OVERFLOW_ERROR);
         assert!(error.message().contains("infinity"));
     }
+
+    // Nearest double with ties to even: 2^47 is half an ulp at 2^100.
+    let x = pow2(100);
+    assert_eq!(x.to_f64().unwrap(), 2f64.powi(100));
+    assert_eq!((&x + pow2(47)).to_f64().unwrap(), 2f64.powi(100));
+    let above = f64::from_bits(2f64.powi(100).to_bits() + 1);
+    assert_eq!((&x + pow2(47) + 1i64).to_f64().unwrap(), above);
+    assert_eq!(big(i64::MIN as i128).to_f64().unwrap(), i64::MIN as f64);
+    assert_eq!((-pow2(100) - 1i64).to_f64().unwrap(), -(2f64.powi(100)));
+    let largest = BigInt::try_from(f64::MAX).unwrap();
+    assert_eq!(largest.to_f64().unwrap(), f64::MAX);
+    assert_eq!((&largest + pow2(970) - 1i64).to_f64().unwrap(), f64::MAX);
+    for value in [&largest + pow2(970), pow2(1024), -pow2(1024), pow2(1100)] {
+        let error = value.to_f64().unwrap_err();
+        assert_eq!(error.kind(), OVERFLOW_ERROR);
+        assert!(error.message().contains("finite double"));
+    }
 }
 
 #[test]
@@ -556,3 +539,676 @@ fn test_big_int_structural_traversal() {
     assert_eq!(leaf, wide());
     assert_eq!(leaf.words().as_ptr(), wide_pointer);
 }
+
+// ============================================================================
+// Arithmetic: ported from tests/cpp/test_big_int.cc.
+// ============================================================================
+
+fn big(value: i128) -> BigInt {
+    BigInt::from(value)
+}
+
+fn pow2(bits: i64) -> BigInt {
+    &big(1) << bits
+}
+
+fn assert_inline(value: &BigInt, expected: i64) {
+    assert_eq!(
+        type_index_of(value),
+        TypeIndex::kTVMFFIInt as i32,
+        "{value}"
+    );
+    assert_eq!(value.to_i64(), Some(expected));
+}
+
+#[test]
+fn test_big_int_promotion_demotion_and_mixed_operands() {
+    let high = &big(i64::MAX as i128) + 1i64;
+    assert_eq!(type_index_of(&high), TypeIndex::kTVMFFIBigInt as i32);
+    assert_eq!(high.to_string(), "9223372036854775808");
+    assert_inline(&(&high - 1i64), i64::MAX);
+    assert_inline(&-&high, i64::MIN);
+    assert_inline(&(&high + i64::MIN), 0);
+    assert_eq!(&big(i64::MIN as i128) - 1i64, -(&high + 1i64));
+    assert_eq!(-big(i64::MIN as i128), high);
+    assert_eq!(&big(i64::MIN as i128) / -1i64, high);
+    assert_inline(&(&big(i64::MIN as i128) % -1i64), 0);
+    assert_eq!(BigInt::from(u64::MAX).to_string(), "18446744073709551615");
+    assert_eq!(1i64 + BigInt::from(u64::MAX), pow2(64));
+    assert_eq!(
+        BigInt::from(u64::MAX) - big(1),
+        &BigInt::from(u64::MAX) - 1i64
+    );
+    for (a, b) in [(i64::MAX, 17i64), (-123, 17)] {
+        let (x, y) = (BigInt::from(a), BigInt::from(b));
+        assert_eq!(&x + &y, &x + b);
+        assert_eq!(&x + &y, a + &y);
+        assert_eq!(&x - &y, &x - b);
+        assert_eq!(&x - &y, a - &y);
+        assert_eq!(&x * &y, &x * b);
+        assert_eq!(&x * &y, a * &y);
+        assert_eq!((&x * &y) / &y, x);
+        assert_eq!(&x / &y, a / &y);
+        assert_eq!(&x / &y, &x / b);
+        assert_eq!(&x % &y, a % &y);
+        assert_eq!(&x % &y, &x % b);
+        assert_eq!(&x & &y, a & &y);
+        assert_eq!(&x | &y, &x | b);
+        assert_eq!(&x ^ &y, a ^ &y);
+        assert_eq!(std::cmp::min(&x, &y), &BigInt::from(a.min(b)));
+        assert_eq!(std::cmp::max(&x, &y), &BigInt::from(a.max(b)));
+    }
+    let heap = pow2(255);
+    assert_eq!(std::cmp::min(heap.clone(), -&heap), -&heap);
+    assert_eq!(std::cmp::max(-&heap, heap.clone()), heap);
+    let mut counter = big(i64::MAX as i128);
+    counter += 1i64;
+    assert_eq!(counter, high);
+    counter -= 1i64;
+    assert_inline(&counter, i64::MAX);
+    counter -= &big(1);
+    assert_inline(&counter, i64::MAX - 1);
+}
+
+#[test]
+fn test_big_int_signed_division() {
+    // (a, b, trunc q, trunc r, floor q, floor r)
+    for (a, b, q, r, floor_q, floor_r) in [
+        (7i64, 3i64, 2i64, 1i64, 2i64, 1i64),
+        (-7, 3, -2, -1, -3, 2),
+        (7, -3, -2, 1, -3, -2),
+        (-7, -3, 2, -1, 2, -1),
+        (-6, 3, -2, 0, -2, 0),
+        (0, 3, 0, 0, 0, 0),
+    ] {
+        let (x, y) = (BigInt::from(a), BigInt::from(b));
+        assert_inline(&(&x / &y), q);
+        assert_inline(&(&x % b), r);
+        assert_inline(&x.try_div(&y).unwrap(), q);
+        assert_inline(&x.try_rem(&y).unwrap(), r);
+        assert_inline(&x.floor_div(&y).unwrap(), floor_q);
+        assert_inline(&x.floor_mod(&y).unwrap(), floor_r);
+        let (dq, dr) = x.div_rem(&y).unwrap();
+        assert_inline(&dq, q);
+        assert_inline(&dr, r);
+        // Wide operands with the same values take the multiword paths.
+        let shift = 200i64;
+        let (wx, wy) = (&x << shift, &y << shift);
+        assert_inline(&(&wx / &wy), q);
+        assert_eq!(&wx % &wy, &BigInt::from(r) << shift);
+        assert_inline(&wx.floor_div(&wy).unwrap(), floor_q);
+        assert_eq!(wx.floor_mod(&wy).unwrap(), &BigInt::from(floor_r) << 
shift);
+    }
+    // An exact multiword quotient from a dividend with low zero words and a 
minimum top word.
+    assert_eq!(
+        (&big(i64::MIN as i128) << 128i64)
+            .div_rem(&pow2(64))
+            .unwrap(),
+        (-pow2(127), big(0))
+    );
+    let wide = pow2(255) + 13i64;
+    for divisor in [big(0), pow2(0) - 1i64] {
+        for result in [
+            wide.try_div(&divisor),
+            wide.try_rem(&divisor),
+            wide.floor_div(&divisor),
+            wide.floor_mod(&divisor),
+            big(0).floor_mod(&divisor),
+            big(5).try_div(&divisor),
+            wide.div_rem(&divisor).map(|pair| pair.0),
+        ] {
+            let error = result.unwrap_err();
+            assert_eq!(error.kind(), ZERO_DIVISION_ERROR);
+            assert_eq!(error.message(), "Division by zero");
+        }
+    }
+}
+
+#[test]
+#[should_panic(expected = "Division by zero")]
+fn test_big_int_division_by_zero_panics() {
+    let _ = &pow2(100) / 0i64;
+}
+
+#[test]
+#[should_panic(expected = "Negative BigInt shift count")]
+fn test_big_int_negative_shift_panics() {
+    let _ = &big(1) << -1i64;
+}
+
+#[test]
+fn test_big_int_wide_arithmetic_and_bitwise() {
+    for shift in [0i64, 63, 64, 255, 256] {
+        let x = pow2(shift);
+        assert_eq!(&x >> shift, big(1));
+        assert_eq!(-&x >> shift, big(-1));
+        assert_inline(&(&x - &x), 0);
+        assert_inline(&((&x + 3i64) - &x), 3);
+        assert_eq!(&x & -1i64, x);
+        assert_eq!(-1i64 & &x, x);
+        assert_eq!(&x | -1i64, big(-1));
+        assert_eq!(!&x, -&x - 1i64);
+        assert_eq!((&x - 1i64) ^ &x, 2i64 * &x - 1i64);
+        assert_eq!((-&x - 1i64) >> shift, big(-2));
+        assert_eq!(&x << big(2), &x * 4i64);
+        assert_eq!(1i64 << BigInt::from(shift), x);
+        assert_eq!(x.try_shl(2).unwrap(), &x * 4i64);
+        assert_eq!(x.try_shr(shift).unwrap(), big(1));
+    }
+    let huge = pow2(255);
+    assert_inline(&(&huge >> &huge), 0);
+    assert_inline(&(-&huge >> &huge), -1);
+    assert_inline(&(&big(0) << &huge), 0);
+    let error = huge.try_shl(huge.clone()).unwrap_err();
+    assert_eq!(error.kind(), OVERFLOW_ERROR);
+    assert_eq!(error.message(), "BigInt shift count is too large");
+    // A count that fits `usize` but not memory is an error, not an abort.
+    for count in [1i64 << 61, i64::MAX] {
+        let error = big(1).try_shl(count).unwrap_err();
+        assert_eq!(error.kind(), OVERFLOW_ERROR);
+        assert_eq!(error.message(), "BigInt allocation is too large");
+    }
+    assert_inline(&huge.try_shr(huge.clone()).unwrap(), 0);
+    let error = huge.try_shl(-1).unwrap_err();
+    assert_eq!(error.kind(), VALUE_ERROR);
+    let error = big(0).try_shr(-1).unwrap_err();
+    assert_eq!(error.kind(), VALUE_ERROR);
+    assert_eq!(pow2(100).to_string(), "1267650600228229401496703205376");
+    // Mixed inline/heap bitwise operands and negative heap values.
+    let negative = -pow2(200) - 5i64;
+    assert_eq!(&negative & 0xffi64, big((-5i128) & 0xff));
+    assert_eq!(&negative | 0i64, negative);
+    assert_eq!(&negative ^ &negative, big(0));
+    assert_eq!(!&negative, pow2(200) + 4i64);
+}
+
+#[test]
+fn test_big_int_independent_wide_fixture() {
+    // Decimal expectations were computed with Python integers, independently 
of these operators.
+    let a: BigInt =
+        
"-57896044618658097711813390734279005840776448950576999782953508207182870753808"
+            .parse()
+            .unwrap();
+    let b: BigInt = 
"1361129467683753853854892183719458155744".parse().unwrap();
+    assert_eq!(
+        (&a + &b).to_string(),
+        
"-57896044618658097711813390734279005839415319482893245929098616023463412598064"
+    );
+    assert_eq!(
+        (&a * &b).to_string(),
+        
"-78804012392788958424676746146478616550595249492184343272504214848687229926200124701108634385188332729146218745073152"
+    );
+    assert_eq!(
+        a.floor_div(&b).unwrap().to_string(),
+        "-42535295865117307932898767499013107221"
+    );
+    assert_eq!(
+        a.floor_mod(&b).unwrap().to_string(),
+        "340250229142126476510862697120718273616"
+    );
+    assert_eq!((&a & &b).to_string(), "72903423425023200");
+    assert_eq!(
+        (&a ^ &b).to_string(),
+        
"-57896044618658097711813390734279005839415319482893245929098761830310262644464"
+    );
+    assert_eq!(
+        (&a >> 97i64).to_string(),
+        "-365375409332725729551097270762435786773001928705"
+    );
+}
+
+#[test]
+fn test_big_int_from_str() {
+    for value in [
+        big(0),
+        big(-1),
+        big(i64::MIN as i128),
+        big(i64::MAX as i128),
+        pow2(63),
+        -pow2(63) - 1i64,
+        pow2(255),
+        -pow2(255),
+        wide(),
+        -wide(),
+        BigInt::from(u128::MAX),
+    ] {
+        let text = value.to_string();
+        assert_eq!(text.parse::<BigInt>().unwrap(), value, "{text}");
+        if !value.is_negative() {
+            assert_eq!(format!("+{text}").parse::<BigInt>().unwrap(), value);
+        }
+    }
+    assert_inline(&"+0007".parse::<BigInt>().unwrap(), 7);
+    assert_inline(&"-0".parse::<BigInt>().unwrap(), 0);
+    assert_eq!(
+        "1267650600228229401496703205376".parse::<BigInt>().unwrap(),
+        pow2(100)
+    );
+    assert_eq!(
+        "000000000000000000001267650600228229401496703205376"
+            .parse::<BigInt>()
+            .unwrap(),
+        pow2(100)
+    );
+    for text in ["", "+", "-", "x", "12x", " 1", "1 ", "1.0", "--1", "0x10"] {
+        let error = text.parse::<BigInt>().unwrap_err();
+        assert_eq!(error.kind(), VALUE_ERROR, "{text:?}");
+    }
+}
+
+#[test]
+fn test_big_int_multiword_division_estimates() {
+    let base = pow2(32);
+    let divisor = pow2(63) + &base - 1i64;
+    let clamp_divisor = pow2(63) + 1i64;
+    let addback_divisor = pow2(95) + 1i64;
+    let shifted_divisor = pow2(64) + 1i64;
+    let wide_quotient = pow2(64) + 7i64;
+    let wide_remainder = pow2(63) + &base + 5i64;
+    // Clamp, one/two estimate corrections, addback, and maximal normalization 
shift.
+    for (a, b, q, r) in [
+        (
+            &clamp_divisor * &base - 1i64,
+            clamp_divisor.clone(),
+            &base - 1i64,
+            pow2(63),
+        ),
+        (
+            &divisor * 2i64 - 1i64,
+            divisor.clone(),
+            big(1),
+            &divisor - 1i64,
+        ),
+        (
+            &divisor * (&base - 2i64) - 1i64,
+            divisor.clone(),
+            &base - 3i64,
+            &divisor - 1i64,
+        ),
+        (
+            &addback_divisor * (&base + 1i64) - 1i64,
+            addback_divisor.clone(),
+            base.clone(),
+            pow2(95),
+        ),
+        (
+            &shifted_divisor * &wide_quotient + &wide_remainder,
+            shifted_divisor.clone(),
+            wide_quotient.clone(),
+            wide_remainder.clone(),
+        ),
+        (&divisor - 1i64, divisor.clone(), big(0), &divisor - 1i64),
+        (divisor.clone(), divisor.clone(), big(1), big(0)),
+    ] {
+        assert_eq!(a.div_rem(&b).unwrap(), (q, r), "{a} / {b}");
+    }
+}
+
+#[test]
+fn test_big_int_scalar_div_rem_boundaries() {
+    let quotient = pow2(192) + 3i64;
+    let dividend = &quotient * 3i64 + 2i64;
+    let maximum_remainder = &quotient * pow2(63) + i64::MAX;
+    let half_max = i64::from(u32::MAX);
+    let half_boundary = (&BigInt::from(half_max) << 64i64) - 1i64;
+    let word_quotient = BigInt::from(u64::MAX);
+    let correction_divisor = 0x400000007fffffffi64;
+    // Zero, exact/nonexact, signed scalar remainder, and the full |INT64_MIN| 
bound.
+    for (a, b, q, r) in [
+        (big(0), 3i64, big(0), 0i64),
+        (big(2), 3, big(0), 2),
+        (quotient.clone(), 1, quotient.clone(), 0),
+        (-&quotient * 3i64, 3, -&quotient, 0),
+        (&quotient * 3i64, 3, quotient.clone(), 0),
+        (dividend.clone(), 3, quotient.clone(), 2),
+        (-&dividend, 3, -&quotient, -2),
+        (maximum_remainder.clone(), i64::MIN, -&quotient, i64::MAX),
+        (-&maximum_remainder, i64::MIN, quotient.clone(), -i64::MAX),
+        (big(i64::MIN as i128), -1, pow2(63), 0),
+        (
+            half_boundary.clone(),
+            half_max,
+            word_quotient.clone(),
+            half_max - 1,
+        ),
+        (
+            -&half_boundary,
+            -half_max,
+            word_quotient.clone(),
+            1 - half_max,
+        ),
+        (
+            (&BigInt::from(half_max + 1) << 64i64) - 1i64,
+            half_max + 1,
+            word_quotient.clone(),
+            half_max,
+        ),
+        // One quotient correction, then two corrections with both overflow 
guards.
+        (pow2(64), half_max + 2, BigInt::from(half_max), 1),
+        (
+            (&BigInt::from(correction_divisor) << 64i64) - 1i64,
+            correction_divisor,
+            word_quotient.clone(),
+            correction_divisor - 1,
+        ),
+    ] {
+        let (dq, dr) = a.div_rem(&BigInt::from(b)).unwrap();
+        assert_eq!(dq, q, "{a} / {b}");
+        assert_inline(&dr, r);
+    }
+}
+
+#[test]
+fn test_big_int_small_divisor_remainders() {
+    let value =
+        pow2(255) | (&BigInt::from(0x12345678i64) << 128i64) | 
BigInt::from(0xfedcba9876543211u64);
+    // Independent signed int32 boundary answers, plus an exact 
negative-divisor remainder.
+    for (a, b, trunc, floor) in [
+        (value.clone(), -2147483648i64, 1985229329i64, -162254319i64),
+        (-&value, 2147483647, -391319368, 1756164279),
+        (-&value, -2147483648, -1985229329, -1985229329),
+        (value.clone(), 2147483647, 391319368, 391319368),
+        (pow2(255), -2147483648, 0, 0),
+    ] {
+        assert_inline(&(&a % b), trunc);
+        assert_inline(&a.floor_mod(&BigInt::from(b)).unwrap(), floor);
+    }
+}
+
+/// Deterministic xorshift64 generator for the differential test.
+struct XorShift(u64);
+
+impl XorShift {
+    fn next(&mut self) -> u64 {
+        let mut x = self.0;
+        x ^= x << 13;
+        x ^= x >> 7;
+        x ^= x << 17;
+        self.0 = x;
+        x
+    }
+
+    /// One to six words mixing carry-sensitive patterns with random bits.
+    fn big_int(&mut self) -> BigInt {
+        let len = (self.next() % 6 + 1) as usize;
+        let words: Vec<i64> = (0..len)
+            .map(|_| match self.next() % 8 {
+                0 => 0,
+                1 => -1,
+                2 => i64::MIN,
+                3 => i64::MAX,
+                4 => 1,
+                _ => self.next() as i64,
+            })
+            .collect();
+        BigInt::from_words(&words)
+    }
+}
+
+fn abs(value: &BigInt) -> BigInt {
+    if value.is_negative() {
+        -value
+    } else {
+        value.clone()
+    }
+}
+
+#[test]
+fn test_big_int_random_algebraic_properties() {
+    let pool = [
+        big(0),
+        big(1),
+        big(-1),
+        big(i64::MIN as i128),
+        big(i64::MAX as i128),
+        pow2(63),
+        -pow2(63),
+        pow2(64),
+        -pow2(64) - 1i64,
+        BigInt::from(u64::MAX),
+        pow2(128),
+        big(i128::MIN),
+        big(i128::MAX),
+        BigInt::from(u128::MAX),
+        wide(),
+        -wide(),
+        BigInt::from_words(&[0, 0, i64::MIN, 1]),
+        pow2(255) + 13i64,
+    ];
+    let mut rng = XorShift(0x9E37_79B9_7F4A_7C15);
+    let pick = |rng: &mut XorShift| {
+        if rng.next() % 4 == 0 {
+            pool[(rng.next() % pool.len() as u64) as usize].clone()
+        } else {
+            rng.big_int()
+        }
+    };
+    let largest_double = BigInt::try_from(f64::MAX).unwrap();
+    for _ in 0..1500 {
+        let a = pick(&mut rng);
+        let b = pick(&mut rng);
+        let context = format!("a = {a}, b = {b}");
+
+        // Ring identities; every result is checked for canonical form by `==`.
+        assert_eq!(&a + &b, &b + &a, "{context}");
+        assert_eq!((&a + &b) - &b, a, "{context}");
+        assert_eq!(&a - &b, &a + -&b, "{context}");
+        assert_eq!(&a * &b, &b * &a, "{context}");
+        assert_eq!(-(&a * &b), -&a * &b, "{context}");
+        assert_eq!(!&a, -&a - 1i64, "{context}");
+        assert_eq!(&a ^ &b, (&a | &b) - (&a & &b), "{context}");
+        assert_eq!((&a & &b) + (&a | &b), &a + &b, "{context}");
+        assert_eq!(a.cmp(&b), (&a - &b).cmp(&big(0)), "{context}");
+
+        // Native integers agree wherever they can represent the operands and 
result.
+        if let (Some(x), Some(y)) = (a.to_i128(), b.to_i128()) {
+            for (expected, actual) in [
+                (x.checked_add(y), &a + &b),
+                (x.checked_sub(y), &a - &b),
+                (x.checked_mul(y), &a * &b),
+                (Some(x & y), &a & &b),
+                (Some(x | y), &a | &b),
+                (Some(x ^ y), &a ^ &b),
+            ] {
+                if let Some(expected) = expected {
+                    assert_eq!(actual, BigInt::from(expected), "{context}");
+                }
+            }
+            if y != 0 && !(x == i128::MIN && y == -1) {
+                assert_eq!(a.try_div(&b).unwrap(), BigInt::from(x / y), 
"{context}");
+                assert_eq!(a.try_rem(&b).unwrap(), BigInt::from(x % y), 
"{context}");
+                assert_eq!(
+                    a.floor_div(&b).unwrap(),
+                    BigInt::from(x.div_euclid(y) - i128::from((y < 0) & 
(x.rem_euclid(y) != 0))),
+                    "{context}"
+                );
+            }
+        }
+
+        // Division identities: a = q*b + r with the truncating and flooring 
sign rules.
+        if b.is_zero() {
+            for result in [
+                a.try_div(&b),
+                a.try_rem(&b),
+                a.floor_div(&b),
+                a.floor_mod(&b),
+            ] {
+                assert_eq!(result.unwrap_err().kind(), ZERO_DIVISION_ERROR);
+            }
+        } else {
+            let (q, r) = a.div_rem(&b).unwrap();
+            assert_eq!(q, a.try_div(&b).unwrap(), "{context}");
+            assert_eq!(r, a.try_rem(&b).unwrap(), "{context}");
+            assert_eq!(&q * &b + &r, a, "{context}");
+            assert!(abs(&r) < abs(&b), "{context}");
+            assert!(
+                r.is_zero() || r.is_negative() == a.is_negative(),
+                "{context}"
+            );
+            let (fq, fr) = (a.floor_div(&b).unwrap(), 
a.floor_mod(&b).unwrap());
+            assert_eq!(&fq * &b + &fr, a, "{context}");
+            assert!(abs(&fr) < abs(&b), "{context}");
+            assert!(
+                fr.is_zero() || fr.is_negative() == b.is_negative(),
+                "{context}"
+            );
+            let rounds_down = !r.is_zero() && a.is_negative() != 
b.is_negative();
+            assert_eq!(
+                fq,
+                if rounds_down { &q - 1i64 } else { q.clone() },
+                "{context}"
+            );
+            assert_eq!((&a * &b).try_div(&b).unwrap(), a, "{context}");
+            assert!((&a * &b).try_rem(&b).unwrap().is_zero(), "{context}");
+        }
+
+        // Shifts are multiplication and floor division by powers of two.
+        let count = (rng.next() % 300) as i64;
+        let shifted = a.try_shl(count).unwrap();
+        assert_eq!(shifted, &a * pow2(count), "{context}, count = {count}");
+        assert_eq!(
+            shifted.try_shr(count).unwrap(),
+            a,
+            "{context}, count = {count}"
+        );
+        assert_eq!(
+            a.try_shr(count).unwrap(),
+            a.floor_div(&pow2(count)).unwrap(),
+            "{context}, count = {count}"
+        );
+        assert_eq!(a.try_shl(-1).unwrap_err().kind(), VALUE_ERROR);
+        assert_eq!(a.try_shr(-1).unwrap_err().kind(), VALUE_ERROR);
+
+        // Text round-trips exactly; the double is the nearest representable 
value.
+        assert_eq!(a.to_string().parse::<BigInt>().unwrap(), a, "{context}");
+        match a.to_f64() {
+            Ok(value) => {
+                assert!(value.is_finite());
+                let nearest = BigInt::try_from(value).unwrap();
+                let difference = abs(&(&a - &nearest));
+                let exponent = ((value.abs().to_bits() >> 52) & 0x7ff) as i64 
- 1075;
+                let ulp = if exponent > 0 { pow2(exponent) } else { big(1) };
+                assert!(
+                    &difference * 2i64 <= ulp,
+                    "{context}: {value} is not the nearest double"
+                );
+                if abs(&a) < pow2(53) {
+                    assert_eq!(nearest, a, "{context}");
+                }
+            }
+            Err(error) => {
+                assert_eq!(error.kind(), OVERFLOW_ERROR);
+                assert!(abs(&a) > largest_double, "{context}");
+            }
+        }
+    }
+}
+
+#[test]
+fn test_big_int_native_objects_cross_runtime() {
+    let echo = echo();
+    // A Rust-allocated object: shared with a C++ container and released by 
C++ last.
+    let product = &wide() * &wide();
+    // wide < 2^256, so the square needs 512 bits: eight words with no sign 
guard.
+    assert_eq!(product.words().len(), 8);
+    assert_eq!(AnyView::from(&product).debug_strong_count(), Some(1));
+    let array = Array::<BigInt>::try_from(
+        Function::get_global("ffi.Array")
+            .unwrap()
+            .call_tuple((&product,))
+            .unwrap(),
+    )
+    .unwrap();
+    assert_eq!(AnyView::from(&product).debug_strong_count(), Some(2));
+    let echoed = 
BigInt::try_from(echo.call_tuple((&product,)).unwrap()).unwrap();
+    assert_eq!(echoed.words().as_ptr(), product.words().as_ptr());
+    let pointer = product.words().as_ptr();
+    drop(product);
+    drop(echoed);
+    let held = array.get(0).unwrap();
+    assert_eq!(held.words().as_ptr(), pointer);
+    assert_eq!(AnyView::from(&held).debug_strong_count(), Some(2));
+    drop(array);
+    assert_eq!(AnyView::from(&held).debug_strong_count(), Some(1));
+
+    // The runtime allocates an equal twin from the Rust-built words.
+    let from_runtime = {
+        let words = held.words();
+        let input = tvm_ffi_sys::TVMFFIByteArray::new(
+            words.as_ptr() as *const u8,
+            std::mem::size_of_val(words),
+        );
+        let mut raw = tvm_ffi_sys::TVMFFIAny::new();
+        assert_eq!(
+            unsafe { tvm_ffi_sys::TVMFFIBigIntFromByteArray(&input, &mut raw) 
},
+            0
+        );
+        BigInt::try_from(unsafe { Any::from_raw_ffi_any(raw) }).unwrap()
+    };
+    assert_ne!(from_runtime.words().as_ptr(), held.words().as_ptr());
+    assert_eq!(from_runtime, held);
+    // C++ hashes and compares the Rust-built key against the runtime-built 
probe.
+    let map: Map<BigInt, i64> = [(held.clone(), 1i64)].into_iter().collect();
+    assert_eq!(map.get(&from_runtime).unwrap(), Some(1));
+    // Rust arithmetic on the runtime-allocated operand; equal values demote 
to inline zero.
+    assert_inline(&(&held - &from_runtime), 0);
+    assert_eq!(&held + &from_runtime, &held << 1i64);
+    drop(map);
+    assert_eq!(AnyView::from(&held).debug_strong_count(), Some(1));
+}
+
+/// Hold a weak reference as C++ `WeakObjectPtr` does (`Object::IncWeakRef`) 
while the
+/// last strong reference to heap `value` goes, then release it 
(`Object::DecWeakRef`).
+/// Returns the (strong, weak) counts the weak reference saw and whether it 
freed.
+fn outlive_with_weak_ref(value: BigInt) -> (u64, u64, bool) {
+    use std::sync::atomic::{fence, Ordering};
+    use tvm_ffi_sys::COMBINED_REF_COUNT_WEAK_ONE as WEAK_ONE;
+    unsafe {
+        let raw = Any::into_raw_ffi_any(Any::from(value));
+        let header = raw.data_union.v_obj;
+        (*header)
+            .combined_ref_count
+            .fetch_add(WEAK_ONE, Ordering::Relaxed);
+        drop(Any::from_raw_ffi_any(raw));
+        let count = (*header).combined_ref_count.load(Ordering::Relaxed);
+        let last = (*header)
+            .combined_ref_count
+            .fetch_sub(WEAK_ONE, Ordering::Release)
+            == WEAK_ONE;
+        if last {
+            fence(Ordering::Acquire);
+            let weak =
+                
tvm_ffi_sys::TVMFFIObjectDeleterFlagBitMask::kTVMFFIObjectDeleterFlagBitMaskWeak;
+            ((*header).deleter.unwrap())(header.cast(), weak as i32);
+        }
+        (count & 0xFFFF_FFFF, count >> 32, last)
+    }
+}
+
+#[test]
+fn test_big_int_objects_outlived_by_weak_ref() {
+    // Both allocation paths: `from_words` (crate allocator) and an operator 
result.
+    for value in [BigInt::from(u64::MAX), &wide() + &wide()] {
+        assert!(value.to_i64().is_none());
+        assert_eq!(outlive_with_weak_ref(value), (0, 1, true));
+    }
+}
+
+#[test]
+fn test_big_int_wide_results_spill_to_heap_buffers() {
+    // Results and division scratch wider than the 32-word stack buffer take 
the heap path.
+    let x = &pow2(3000) + 12345i64;
+    assert_eq!(x.words().len(), 47);
+    let square = &x * &x;
+    assert_eq!(square.words().len(), 94);
+    let (quotient, remainder) = square.div_rem(&x).unwrap();
+    assert_eq!(quotient, x);
+    assert_inline(&remainder, 0);
+    assert_eq!(&x % &pow2(3000), big(12345));
+    assert_eq!(&x + &x, &x << 1i64);
+    assert_inline(&(&(-&x) + &x), 0);
+    assert_eq!(&(&x << 100i64) >> 100i64, x);
+    assert_eq!(&x & &x, x);
+    assert_inline(&(&x ^ &x), 0);
+    assert_eq!(x.to_string().parse::<BigInt>().unwrap(), x);
+}
diff --git a/rust/tvm-ffi/tests/test_object.rs 
b/rust/tvm-ffi/tests/test_object.rs
index ebab18e0..a5cb1a86 100644
--- a/rust/tvm-ffi/tests/test_object.rs
+++ b/rust/tvm-ffi/tests/test_object.rs
@@ -123,6 +123,122 @@ fn test_object_arc_with_extra_items() {
     assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
 }
 
+// Records the size of this thread's latest deallocation, to check the layout 
an
+// object is released with.
+struct RecordingAllocator;
+
+thread_local! {
+    static LAST_DEALLOC_SIZE: std::cell::Cell<usize> = const { 
std::cell::Cell::new(0) };
+}
+
+unsafe impl std::alloc::GlobalAlloc for RecordingAllocator {
+    unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
+        std::alloc::System.alloc(layout)
+    }
+
+    unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
+        let _ = LAST_DEALLOC_SIZE.try_with(|size| size.set(layout.size()));
+        std::alloc::System.dealloc(ptr, layout)
+    }
+}
+
+#[global_allocator]
+static ALLOCATOR: RecordingAllocator = RecordingAllocator;
+
+/// Drops `obj_arc` while a weak reference outlives it, held the way C++ 
`WeakObjectPtr`
+/// does (`Object::IncWeakRef`, then `Object::DecWeakRef`). `expired` runs 
while only the
+/// weak reference is left. Returns the size the allocation is freed with.
+fn release_outlived_by_weak_ref<T: ObjectCore>(
+    mut obj_arc: ObjectArc<T>,
+    expired: impl FnOnce(),
+) -> usize {
+    use std::sync::atomic::fence;
+    use tvm_ffi_sys::COMBINED_REF_COUNT_WEAK_ONE as WEAK_ONE;
+    unsafe {
+        let header = ObjectArc::as_raw_mut(&mut obj_arc) as *mut TVMFFIObject;
+        (*header)
+            .combined_ref_count
+            .fetch_add(WEAK_ONE, Ordering::Relaxed);
+        drop(obj_arc);
+        // The data is dropped; the weak reference still sees an intact, 
expired header.
+        assert_eq!(
+            (*header).combined_ref_count.load(Ordering::Relaxed),
+            WEAK_ONE
+        );
+        expired();
+        // The last weak reference frees the full allocation.
+        let old = (*header)
+            .combined_ref_count
+            .fetch_sub(WEAK_ONE, Ordering::Release);
+        assert_eq!(old, WEAK_ONE);
+        fence(Ordering::Acquire);
+        let weak = 
tvm_ffi_sys::TVMFFIObjectDeleterFlagBitMask::kTVMFFIObjectDeleterFlagBitMaskWeak;
+        ((*header).deleter.unwrap())(header.cast(), weak as i32);
+    }
+    LAST_DEALLOC_SIZE.with(|size| size.get())
+}
+
+#[test]
+fn test_object_arc_with_extra_items_outlived_by_weak_ref() {
+    let delete_counter = Arc::new(AtomicU32::new(0));
+    let obj_arc = ObjectArc::new_with_extra_items(TestIntObj::new(13, 
delete_counter.clone(), 10));
+    let freed = release_outlived_by_weak_ref(obj_arc, || {
+        assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
+    });
+    assert_eq!(
+        freed,
+        std::mem::size_of::<TestIntObj>() + 10 * std::mem::size_of::<u64>()
+    );
+}
+
+// Only the header, with a fixed number of items after it.
+#[repr(C)]
+struct HeaderOnlyObj<const N: usize> {
+    object: Object,
+}
+
+unsafe impl<const N: usize> ObjectCore for HeaderOnlyObj<N> {
+    const TYPE_KEY: &'static str = Object::TYPE_KEY;
+    const TYPE_DEPTH: i32 = Object::TYPE_DEPTH;
+    #[inline]
+    fn type_index() -> i32 {
+        Object::type_index()
+    }
+    #[inline]
+    unsafe fn object_header_mut(this: &mut Self) -> &mut TVMFFIObject {
+        Object::object_header_mut(&mut this.object)
+    }
+}
+
+unsafe impl<const N: usize> ObjectCoreWithExtraItems for HeaderOnlyObj<N> {
+    type ExtraItem = u64;
+    #[inline]
+    fn extra_items_count(_: &Self) -> usize {
+        N
+    }
+}
+
+#[test]
+fn test_header_only_object_with_extra_items_outlived_by_weak_ref() {
+    // The item count is kept in the word after the header: the first item for 
N = 2,
+    // padding the allocation reserves for N = 0.
+    let header_size = std::mem::size_of::<TVMFFIObject>();
+    let obj_arc = ObjectArc::new_with_extra_items(HeaderOnlyObj::<2> {
+        object: Object::new(),
+    });
+    assert_eq!(
+        release_outlived_by_weak_ref(obj_arc, || {}),
+        header_size + 16
+    );
+    let obj_arc = ObjectArc::new_with_extra_items(HeaderOnlyObj::<0> {
+        object: Object::new(),
+    });
+    assert_eq!(
+        release_outlived_by_weak_ref(obj_arc, || {}),
+        header_size + 8
+    );
+}
+
 #[test]
 fn test_object_arc_from_raw() {
     unsafe {

Reply via email to