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 13865e1c [FEAT][RUST] Add BigInt to the Rust crate, backed by the C++ 
runtime (#804)
13865e1c is described below

commit 13865e1c5921d95563275b14816b46975b1d4117
Author: Linzhang Li <[email protected]>
AuthorDate: Sun Sep 27 18:04:33 2026 -0400

    [FEAT][RUST] Add BigInt to the Rust crate, backed by the C++ runtime (#804)
    
    [FEAT][RUST] Add BigInt to the Rust crate, backed by the C++ runtime
    
    ## Summary
    
    `tvm_ffi::BigInt` 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. Heap values are always built through
    `TVMFFIBigIntFromByteArray`,
    so the C++ runtime keeps the representation canonical. `words()` reads
    the
    object natively through a `#[repr(C)]` `BigIntObj` that matches the C++
    layout (object header, `size`, trailing words) instead of paying an FFI
    call
    per access; `TVMFFIBigIntGetContentByteArray` is declared in
    `tvm-ffi-sys`
    and used by a test that cross-checks the mirror against the runtime.
    
    - Conversions: `From` for every primitive integer width, `bool`, `i128`
    and
      `u128`; `TryFrom<f64>` truncates toward zero (`NaN` -> `ValueError`,
    infinity -> a new `OVERFLOW_ERROR` kind); `to_i64` / `to_u64` /
    `to_i128` /
      `to_u128` return `None` when the value does not fit.
    - Value semantics: `Clone` / `Drop` manage the object reference; `Eq`
    compares
    words (the canonical form makes that exact), `Ord` is sign-aware, `Hash`
      agrees with `Eq`; `Display` / `Debug` print decimal.
    - FFI: `AnyCompatible` accepts `Int` and `BigInt` strictly, and `Bool`
    on
      cast, like C++ `TypeTraits<BigInt>`, so a typed Rust function taking
    `BigInt` also receives plain ints. `TryFrom<Any>` / `TryFrom<AnyView>`,
    `Optional<BigInt>` and the typed-call argument traits are registered the
    same way as `String`. Structural visit and mutate treat `BigIntObj` as a
      leaf; it has no reflected fields.
    - The Rust stub generator's default type map gains
      `ffi.BigInt -> tvm_ffi::BigInt`, with a matching assert.
    
    This PR only transports and converts values; arithmetic stays with the
    C++
    implementation. Two follow-up PRs are staged on top of it: a native port
    of
    the C++ `int_ops` (operators, `try_*` forms, `FromStr`), and
    stub-generator
    support for `BigInt` fields of reflected classes.
    
    ## Testing
    
    - `cargo test -p tvm-ffi`: all suites green. `tests/test_big_int.rs`
    adds
    **14 tests**: the inline/heap boundary at ±2^63, `from_words`
    normalization
    (redundant sign words pruned, `i64`-range values demoted to inline), the
    `words()` mirror cross-checked against
    `TVMFFIBigIntGetContentByteArray`,
    every native conversion including the `f64` edge cases, ordering and
    hash,
      decimal display, `Any` / `AnyView` round trips, a round trip through a
    runtime function, `Array` / `Map` containers, `Optional`, and structural
      traversal.
    - `rustfmt` and `clippy` clean on the new files.
    - No C++ changes.
    
    ---------
    
    Signed-off-by: yuchuan <[email protected]>
---
 python/tvm_ffi/stub/rust_generator/consts.py |   1 +
 rust/tvm-ffi-sys/src/c_api.rs                |   2 +
 rust/tvm-ffi/src/any.rs                      |   1 +
 rust/tvm-ffi/src/big_int.rs                  | 429 ++++++++++++++++++++
 rust/tvm-ffi/src/error.rs                    |   1 +
 rust/tvm-ffi/src/function_internal.rs        |   2 +
 rust/tvm-ffi/src/lib.rs                      |   5 +-
 rust/tvm-ffi/src/optional.rs                 |   1 +
 rust/tvm-ffi/tests/test_big_int.rs           | 558 +++++++++++++++++++++++++++
 tests/python/test_stubgen_rust.py            |   1 +
 10 files changed, 1000 insertions(+), 1 deletion(-)

diff --git a/python/tvm_ffi/stub/rust_generator/consts.py 
b/python/tvm_ffi/stub/rust_generator/consts.py
index b6543dbf..be644607 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -45,6 +45,7 @@ RUST_TY_MAP_DEFAULTS = {
     # --- builtin object type keys (ffi.*) ---
     "ffi.String": "tvm_ffi::String",
     "ffi.Bytes": "tvm_ffi::Bytes",
+    "ffi.BigInt": "tvm_ffi::BigInt",
     "ffi.Module": "tvm_ffi::Module",
     "ffi.Error": "tvm_ffi::Error",
     "ffi.Object": "tvm_ffi::object::ObjectRef",
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index 454956ac..f7b7c334 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -529,6 +529,8 @@ unsafe extern "C" {
     pub fn TVMFFITensorToDLPackVersioned(from: TVMFFIObjectHandle, out: *mut 
*mut c_void) -> i32;
     pub fn TVMFFIStringFromByteArray(input: *const TVMFFIByteArray, out: *mut 
TVMFFIAny) -> i32;
     pub fn TVMFFIBytesFromByteArray(input: *const TVMFFIByteArray, out: *mut 
TVMFFIAny) -> i32;
+    pub fn TVMFFIBigIntFromByteArray(input: *const TVMFFIByteArray, out: *mut 
TVMFFIAny) -> i32;
+    pub fn TVMFFIBigIntGetContentByteArray(value: *const TVMFFIAny) -> 
TVMFFIByteArray;
     pub fn TVMFFIDataTypeFromString(str: *const TVMFFIByteArray, out: *mut 
DLDataType) -> i32;
     pub fn TVMFFIDataTypeToString(dtype: *const DLDataType, out: *mut 
TVMFFIAny) -> i32;
     pub fn TVMFFITraceback(
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 7c9b9d92..2b21a127 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -417,6 +417,7 @@ crate::impl_try_from_any!(
     *mut core::ffi::c_void,
     crate::string::String,
     crate::string::Bytes,
+    crate::big_int::BigInt,
     crate::object::ObjectRef,
     tvm_ffi_sys::dlpack::DLDataType,
     tvm_ffi_sys::dlpack::DLDevice,
diff --git a/rust/tvm-ffi/src/big_int.rs b/rust/tvm-ffi/src/big_int.rs
new file mode 100644
index 00000000..fbcb750d
--- /dev/null
+++ b/rust/tvm-ffi/src/big_int.rs
@@ -0,0 +1,429 @@
+/*
+ * 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/error.rs b/rust/tvm-ffi/src/error.rs
index f75f18c3..f540324a 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -48,6 +48,7 @@ pub const ATTRIBUTE_ERROR: ErrorKind = 
ErrorKind("AttributeError");
 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");
 
 /// error object
 #[repr(C)]
diff --git a/rust/tvm-ffi/src/function_internal.rs 
b/rust/tvm-ffi/src/function_internal.rs
index 328766c7..34cdeec7 100644
--- a/rust/tvm-ffi/src/function_internal.rs
+++ b/rust/tvm-ffi/src/function_internal.rs
@@ -122,6 +122,7 @@ crate::impl_into_arg_holder_default!(
     f64,
     String,
     Bytes,
+    crate::big_int::BigInt,
     Any,
     crate::DLDataType,
     crate::DLDevice
@@ -231,6 +232,7 @@ crate::impl_arg_into_ref!(
     f64,
     String,
     Bytes,
+    crate::big_int::BigInt,
     Any,
     crate::DLDataType,
     crate::DLDevice
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index a4ddfbda..f90c88b2 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -17,6 +17,7 @@
  * under the License.
  */
 pub mod any;
+pub mod big_int;
 pub mod collections;
 pub mod derive;
 pub mod device;
@@ -37,6 +38,7 @@ pub mod type_traits;
 pub use tvm_ffi_sys;
 
 pub use crate::any::{Any, AnyView};
+pub use crate::big_int::BigInt;
 pub use crate::collections::array::Array;
 pub use crate::collections::map::Map;
 pub use crate::collections::shape::Shape;
@@ -45,7 +47,8 @@ pub use crate::device::{current_stream, with_stream};
 pub use crate::dtype::DLDataTypeExt;
 pub use crate::error::{Error, ErrorKind, Result};
 pub use crate::error::{
-    ATTRIBUTE_ERROR, INDEX_ERROR, INTERNAL_ERROR, KEY_ERROR, RUNTIME_ERROR, 
TYPE_ERROR, VALUE_ERROR,
+    ATTRIBUTE_ERROR, INDEX_ERROR, INTERNAL_ERROR, KEY_ERROR, OVERFLOW_ERROR, 
RUNTIME_ERROR,
+    TYPE_ERROR, VALUE_ERROR,
 };
 pub use crate::extra::module::Module;
 pub use crate::extra::structural_mutate::{
diff --git a/rust/tvm-ffi/src/optional.rs b/rust/tvm-ffi/src/optional.rs
index 907127df..111eaace 100644
--- a/rust/tvm-ffi/src/optional.rs
+++ b/rust/tvm-ffi/src/optional.rs
@@ -83,6 +83,7 @@ impl_optional_compatible!(
     crate::DLDevice,
     String,
     Bytes,
+    crate::big_int::BigInt,
 );
 
 unsafe impl<T: OptionalCompatible> OptionalCompatible for Option<T> {}
diff --git a/rust/tvm-ffi/tests/test_big_int.rs 
b/rust/tvm-ffi/tests/test_big_int.rs
new file mode 100644
index 00000000..a64baf38
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_big_int.rs
@@ -0,0 +1,558 @@
+/*
+ * 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 std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
+use tvm_ffi::*;
+
+// (1 << 255) | (0x0123456789ABCDEF << 128) | 0xFEDCBA9876543210: distinct 
words, an
+// interior zero, and a top word whose sign bit needs the zero guard word.
+const WIDE_WORDS: [i64; 5] = [
+    0xFEDCBA9876543210_u64 as i64,
+    0,
+    0x0123456789ABCDEF,
+    i64::MIN,
+    0,
+];
+const WIDE_DECIMAL: &str =
+    
"57896044618658097711813390734279005840776448950576999782953508207182870753808";
+
+fn wide() -> BigInt {
+    BigInt::from_words(&WIDE_WORDS)
+}
+
+/// The dummy call keeps `libtvm_ffi_testing`, which registers `testing.echo`, 
linked.
+fn echo() -> Function {
+    assert_eq!(unsafe { tvm_ffi_sys::TVMFFITestingDummyTarget() }, 0);
+    Function::get_global("testing.echo").unwrap()
+}
+
+fn type_index_of(value: &BigInt) -> i32 {
+    AnyView::from(value).type_index()
+}
+
+fn hash_of(value: &BigInt) -> u64 {
+    let mut hasher = DefaultHasher::new();
+    value.hash(&mut hasher);
+    hasher.finish()
+}
+
+#[test]
+fn test_big_int_inline_representation() {
+    for value in [BigInt::new(), BigInt::default(), BigInt::from(0i64)] {
+        assert_eq!(type_index_of(&value), TypeIndex::kTVMFFIInt as i32);
+        assert_eq!(value.words(), &[0]);
+        assert_eq!(value.to_i64(), Some(0));
+    }
+    let inline = [
+        BigInt::from(-1i8),
+        BigInt::from(i16::MIN),
+        BigInt::from(u32::MAX),
+        BigInt::from(i64::MAX),
+        BigInt::from(i64::MIN),
+        BigInt::from(usize::MAX >> 1),
+        BigInt::from(i128::from(i64::MAX)),
+        BigInt::from(u128::from(u64::MAX >> 1)),
+    ];
+    let expected = [
+        -1,
+        i64::from(i16::MIN),
+        i64::from(u32::MAX),
+        i64::MAX,
+        i64::MIN,
+        (usize::MAX >> 1) as i64,
+        i64::MAX,
+        i64::MAX,
+    ];
+    for (value, expected) in inline.iter().zip(expected) {
+        assert_eq!(type_index_of(value), TypeIndex::kTVMFFIInt as i32);
+        assert_eq!(value.to_i64(), Some(expected));
+        assert_eq!(value.words(), &[expected]);
+        assert_eq!(value.is_negative(), expected < 0);
+    }
+}
+
+#[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() {
+    let cases = [
+        (0.0, BigInt::from(0i64)),
+        (-0.0, BigInt::from(0i64)),
+        (1.9, BigInt::from(1i64)),
+        (-1.9, BigInt::from(-1i64)),
+        (-0.999, BigInt::from(0i64)),
+        (123456789.987, BigInt::from(123456789i64)),
+        (9007199254740994.0, BigInt::from(9007199254740994i64)),
+        (i64::MIN as f64, BigInt::from(i64::MIN)),
+        // i64::MAX rounds up to 2^63 as a double, which no longer fits inline.
+        (i64::MAX as f64, BigInt::from(1u128 << 63)),
+        (-(i64::MAX as f64), BigInt::from(i64::MIN)),
+        (18446744073709551616.0, BigInt::from(1u128 << 64)),
+        (1e30, BigInt::from(1000000000000000019884624838656i128)),
+        (
+            -1.5 * 2f64.powi(100),
+            BigInt::from(-1901475900342344102245054808064i128),
+        ),
+    ];
+    for (input, expected) in cases {
+        assert_eq!(BigInt::try_from(input).unwrap(), expected, "{input}");
+    }
+    // Wide magnitudes: the significand straddles words at every alignment.
+    for input in [
+        1e300,
+        -1e300,
+        f64::MAX,
+        -f64::MAX,
+        1.75 * 2f64.powi(1000),
+        -(2f64.powi(1023)),
+        3.0 * 2f64.powi(191),
+    ] {
+        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!(
+        BigInt::try_from(3.0 * 2f64.powi(191)).unwrap().words(),
+        &[0, 0, i64::MIN, 1]
+    );
+    let error = BigInt::try_from(f64::NAN).unwrap_err();
+    assert_eq!(error.kind(), VALUE_ERROR);
+    assert!(error.message().contains("NaN"));
+    for input in [f64::INFINITY, f64::NEG_INFINITY] {
+        let error = BigInt::try_from(input).unwrap_err();
+        assert_eq!(error.kind(), OVERFLOW_ERROR);
+        assert!(error.message().contains("infinity"));
+    }
+}
+
+#[test]
+fn test_big_int_heap_representation() {
+    let heap = [
+        (BigInt::from(u64::MAX), vec![-1, 0]),
+        (BigInt::from(usize::MAX), vec![-1, 0]),
+        (BigInt::from(1u128 << 63), vec![i64::MIN, 0]),
+        (BigInt::from(i128::from(i64::MIN) - 1), vec![i64::MAX, -1]),
+        (BigInt::from(i128::MIN), vec![0, i64::MIN]),
+        (BigInt::from(u128::MAX), vec![-1, -1, 0]),
+        (wide(), WIDE_WORDS.to_vec()),
+    ];
+    for (value, words) in heap {
+        assert_eq!(type_index_of(&value), TypeIndex::kTVMFFIBigInt as i32);
+        assert_eq!(value.words(), words.as_slice());
+        assert_eq!(value.to_i64(), None);
+        assert_eq!(value.is_negative(), words[words.len() - 1] < 0);
+        assert_eq!(AnyView::from(&value).debug_strong_count(), Some(1));
+        let copy = value.clone();
+        assert_eq!(AnyView::from(&value).debug_strong_count(), Some(2));
+        assert_eq!(copy.words().as_ptr(), value.words().as_ptr());
+        drop(copy);
+        assert_eq!(AnyView::from(&value).debug_strong_count(), Some(1));
+    }
+}
+
+#[test]
+fn test_big_int_words_mirror_runtime_content() {
+    // The native word slice must be the runtime's own view of the same cell.
+    for value in [
+        wide(),
+        BigInt::from(u64::MAX),
+        BigInt::from(i128::MIN),
+        BigInt::from(-5i64),
+    ] {
+        let mut any = Any::from(value.clone());
+        let content = unsafe { 
tvm_ffi_sys::TVMFFIBigIntGetContentByteArray(any.as_data_ptr()) };
+        let runtime = unsafe {
+            std::slice::from_raw_parts(
+                content.data as *const i64,
+                content.size / std::mem::size_of::<i64>(),
+            )
+        };
+        assert_eq!(value.words(), runtime);
+        // A heap value is shared, so both views borrow the same object words.
+        if value.to_i64().is_none() {
+            assert_eq!(value.words().as_ptr(), runtime.as_ptr());
+        }
+    }
+}
+
+#[test]
+fn test_big_int_from_words_normalizes() {
+    let zero = BigInt::from_words(&[]);
+    assert_eq!(type_index_of(&zero), TypeIndex::kTVMFFIInt as i32);
+    assert_eq!(zero, BigInt::new());
+    for (words, expected) in [
+        (vec![5, 0, 0], 5),
+        (vec![-5, -1, -1], -5),
+        (vec![i64::MIN, -1], i64::MIN),
+        (vec![i64::MAX, 0], i64::MAX),
+    ] {
+        let value = BigInt::from_words(&words);
+        assert_eq!(type_index_of(&value), TypeIndex::kTVMFFIInt as i32);
+        assert_eq!(value.to_i64(), Some(expected));
+    }
+    let padded = BigInt::from_words(&[-1, 0, 0, 0]);
+    assert_eq!(type_index_of(&padded), TypeIndex::kTVMFFIBigInt as i32);
+    assert_eq!(padded.words(), &[-1, 0]);
+    assert_eq!(padded, BigInt::from(u64::MAX));
+    let mut padded_wide = WIDE_WORDS.to_vec();
+    padded_wide.extend([0, 0]);
+    assert_eq!(BigInt::from_words(&padded_wide).words(), &WIDE_WORDS);
+    let mut padded_negative = WIDE_WORDS.map(|word| !word).to_vec();
+    padded_negative.extend([-1, -1]);
+    assert_eq!(
+        BigInt::from_words(&padded_negative).words(),
+        &WIDE_WORDS.map(|word| !word)
+    );
+}
+
+#[test]
+fn test_big_int_native_conversions() {
+    let cases: [(BigInt, Option<i64>, Option<u64>, Option<i128>, 
Option<u128>); 9] = [
+        (BigInt::from(0i64), Some(0), Some(0), Some(0), Some(0)),
+        (BigInt::from(-1i64), Some(-1), None, Some(-1), None),
+        (
+            BigInt::from(i64::MAX),
+            Some(i64::MAX),
+            Some(i64::MAX as u64),
+            Some(i128::from(i64::MAX)),
+            Some(u128::from(i64::MAX as u64)),
+        ),
+        (
+            BigInt::from(i64::MIN),
+            Some(i64::MIN),
+            None,
+            Some(i128::from(i64::MIN)),
+            None,
+        ),
+        (
+            BigInt::from(u64::MAX),
+            None,
+            Some(u64::MAX),
+            Some(i128::from(u64::MAX)),
+            Some(u128::from(u64::MAX)),
+        ),
+        (
+            BigInt::from(i128::from(i64::MIN) - 1),
+            None,
+            None,
+            Some(i128::from(i64::MIN) - 1),
+            None,
+        ),
+        (
+            BigInt::from(i128::MAX),
+            None,
+            None,
+            Some(i128::MAX),
+            Some(i128::MAX as u128),
+        ),
+        (BigInt::from(i128::MIN), None, None, Some(i128::MIN), None),
+        (BigInt::from(u128::MAX), None, None, None, Some(u128::MAX)),
+    ];
+    for (value, i64_value, u64_value, i128_value, u128_value) in cases {
+        assert_eq!(value.to_i64(), i64_value, "{value}");
+        assert_eq!(value.to_u64(), u64_value, "{value}");
+        assert_eq!(value.to_i128(), i128_value, "{value}");
+        assert_eq!(value.to_u128(), u128_value, "{value}");
+    }
+    assert_eq!(wide().to_u128(), None);
+    assert_eq!(wide().to_i128(), None);
+}
+
+#[test]
+fn test_big_int_ordering_and_hash() {
+    let negative_wide = BigInt::from_words(&WIDE_WORDS.map(|word| !word));
+    let ascending = [
+        negative_wide.clone(),
+        BigInt::from(i128::MIN),
+        BigInt::from(-(1i128 << 64)),
+        BigInt::from(i128::from(i64::MIN) - 1),
+        BigInt::from(i64::MIN),
+        BigInt::from(-1i64),
+        BigInt::from(0i64),
+        BigInt::from(1i64),
+        BigInt::from(i64::MAX),
+        BigInt::from(1u128 << 63),
+        BigInt::from(u64::MAX),
+        BigInt::from(1u128 << 64),
+        BigInt::from(i128::MAX),
+        BigInt::from(u128::MAX),
+        wide(),
+    ];
+    for (i, lhs) in ascending.iter().enumerate() {
+        for (j, rhs) in ascending.iter().enumerate() {
+            assert_eq!(lhs.cmp(rhs), i.cmp(&j), "{lhs} vs {rhs}");
+            assert_eq!(lhs == rhs, i == j, "{lhs} vs {rhs}");
+        }
+    }
+    let same = [
+        (BigInt::from(u64::MAX), BigInt::from_words(&[-1, 0, 0])),
+        (BigInt::from(7u8), BigInt::from_words(&[7, 0])),
+        (wide(), BigInt::from_words(&WIDE_WORDS)),
+        (
+            negative_wide,
+            BigInt::from_words(&WIDE_WORDS.map(|word| !word)),
+        ),
+    ];
+    for (lhs, rhs) in same {
+        assert_eq!(lhs, rhs);
+        assert_eq!(hash_of(&lhs), hash_of(&rhs));
+    }
+}
+
+#[test]
+fn test_big_int_display() {
+    let cases = [
+        (BigInt::from(0i64), "0"),
+        (BigInt::from(-1i64), "-1"),
+        (BigInt::from(i64::MAX), "9223372036854775807"),
+        (BigInt::from(i64::MIN), "-9223372036854775808"),
+        (BigInt::from(1u128 << 63), "9223372036854775808"),
+        (
+            BigInt::from(i128::from(i64::MIN) - 1),
+            "-9223372036854775809",
+        ),
+        (BigInt::from(u64::MAX), "18446744073709551615"),
+        (BigInt::from(1u128 << 64), "18446744073709551616"),
+        (
+            BigInt::from(1u128 << 100),
+            "1267650600228229401496703205376",
+        ),
+        (
+            BigInt::from(-(1i128 << 100)),
+            "-1267650600228229401496703205376",
+        ),
+        (
+            BigInt::from(i128::MIN),
+            "-170141183460469231731687303715884105728",
+        ),
+        (
+            BigInt::from(u128::MAX),
+            "340282366920938463463374607431768211455",
+        ),
+        (
+            BigInt::from_words(&[0, 0, 0, 1]),
+            "6277101735386680763835789423207666416102355444464034512896",
+        ),
+        (wide(), WIDE_DECIMAL),
+    ];
+    for (value, expected) in cases {
+        assert_eq!(value.to_string(), expected);
+        assert_eq!(
+            format!("{value:?}"),
+            format!("ffi.BigInt {{ data: {expected} }}")
+        );
+    }
+    assert_eq!(
+        format!("{:>22}", BigInt::from(u64::MAX)),
+        "  18446744073709551615"
+    );
+    assert_eq!(
+        format!("{:+}", BigInt::from(1u128 << 64)),
+        "+18446744073709551616"
+    );
+}
+
+#[test]
+fn test_big_int_any_round_trip() {
+    let value = wide();
+    let any = Any::from(value.clone());
+    assert_eq!(any.type_index(), TypeIndex::kTVMFFIBigInt as i32);
+    assert_eq!(any.debug_strong_count(), Some(2));
+    assert_eq!(any.try_as::<BigInt>(), Some(value.clone()));
+    let view = AnyView::from(&value);
+    assert_eq!(BigInt::try_from(view).unwrap(), value);
+    let restored = BigInt::try_from(any).unwrap();
+    assert_eq!(restored.words().as_ptr(), value.words().as_ptr());
+    drop(restored);
+    assert_eq!(AnyView::from(&value).debug_strong_count(), Some(1));
+
+    // An inline value is a plain int cell in both directions.
+    let small = BigInt::from(-42i64);
+    let any = Any::from(small.clone());
+    assert_eq!(any.type_index(), TypeIndex::kTVMFFIInt as i32);
+    assert_eq!(i64::try_from(any).unwrap(), -42);
+    assert_eq!(BigInt::try_from(Any::from(-42i64)).unwrap(), small);
+    assert_eq!(Any::from(7i32).try_as::<BigInt>(), Some(BigInt::from(7i64)));
+    assert_eq!(
+        BigInt::try_from(Any::from(true)).unwrap(),
+        BigInt::from(1i64)
+    );
+    assert_eq!(Any::from(true).try_as::<BigInt>(), None);
+
+    let error = BigInt::try_from(Any::from(1.5f64)).unwrap_err();
+    assert_eq!(error.kind(), TYPE_ERROR);
+    assert!(error.message().contains("BigInt"));
+    let error = i64::try_from(Any::from(wide())).unwrap_err();
+    assert_eq!(error.kind(), TYPE_ERROR);
+    assert!(error.message().contains("ffi.BigInt"));
+}
+
+#[test]
+fn test_big_int_function_round_trip() {
+    let echo = echo();
+    for value in [wide(), BigInt::from(i128::MIN), BigInt::from(i64::MIN)] {
+        let result = echo.call_tuple((value.clone(),)).unwrap();
+        assert_eq!(result.type_index(), type_index_of(&value));
+        let echoed = BigInt::try_from(result).unwrap();
+        assert_eq!(echoed, value);
+        // A heap value crosses the call by reference; an inline one is copied.
+        if value.to_i64().is_none() {
+            assert_eq!(echoed.words().as_ptr(), value.words().as_ptr());
+        }
+    }
+
+    let identity = Function::from_typed(|value: BigInt| -> Result<BigInt> { 
Ok(value) });
+    let result = identity.call_tuple((wide(),)).unwrap();
+    assert_eq!(BigInt::try_from(result).unwrap(), wide());
+    let result = identity.call_tuple((5i64,)).unwrap();
+    assert_eq!(result.type_index(), TypeIndex::kTVMFFIInt as i32);
+    assert_eq!(BigInt::try_from(result).unwrap(), BigInt::from(5i64));
+    let error = identity.call_tuple((String::from("text"),)).err().unwrap();
+    assert_eq!(error.kind(), TYPE_ERROR);
+    assert!(error.message().contains("BigInt"));
+
+    let narrow = Function::from_typed(|value: i64| -> Result<i64> { Ok(value) 
});
+    let error = narrow.call_tuple((&wide(),)).err().unwrap();
+    assert_eq!(error.kind(), TYPE_ERROR);
+    assert!(error.message().contains("ffi.BigInt"));
+}
+
+#[test]
+fn test_big_int_containers() {
+    let values = vec![wide(), BigInt::from(-1i64), BigInt::from(u64::MAX)];
+    let array = Array::new(values.clone());
+    assert_eq!(array.len(), 3);
+    for (i, value) in values.iter().enumerate() {
+        assert_eq!(array.get(i).unwrap(), *value);
+    }
+    let echoed = echo()
+        .call_tuple((array.clone(),))
+        .and_then(Array::<BigInt>::try_from)
+        .unwrap();
+    assert_eq!(echoed.iter().collect::<Vec<_>>(), values);
+
+    let map: Map<BigInt, i64> = values
+        .iter()
+        .enumerate()
+        .map(|(i, value)| (value.clone(), i as i64))
+        .collect();
+    assert_eq!(map.len(), 3);
+    for (i, value) in values.iter().enumerate() {
+        assert!(map.contains_key(value));
+        assert_eq!(map.get(value).unwrap(), Some(i as i64));
+    }
+    assert_eq!(map.get(&BigInt::from(1u128 << 64)).unwrap(), None);
+}
+
+#[test]
+fn test_big_int_optional() {
+    assert_eq!(std::mem::size_of::<Optional<BigInt>>(), 16);
+    let some = Optional::some(wide());
+    assert!(some.has_value());
+    assert_eq!(some.get(), Some(wide()));
+    let none = Optional::<BigInt>::none();
+    assert!(none.is_none());
+    assert_eq!(none.get(), None);
+    assert_eq!(some.into_option(), Some(wide()));
+
+    let any = Any::from(Some(wide()));
+    assert_eq!(any.type_index(), TypeIndex::kTVMFFIBigInt as i32);
+    assert_eq!(Option::<BigInt>::try_from(any).unwrap(), Some(wide()));
+    assert_eq!(Option::<BigInt>::try_from(Any::new()).unwrap(), None);
+    let echoed = echo().call_tuple((Some(BigInt::from(u64::MAX)),)).unwrap();
+    assert_eq!(
+        Option::<BigInt>::try_from(echoed).unwrap(),
+        Some(BigInt::from(u64::MAX))
+    );
+}
+
+#[test]
+fn test_big_int_structural_traversal() {
+    let root = Array::new(vec![Any::from(1i64), Any::from(wide())]);
+    let wide_pointer = root
+        .get(1)
+        .unwrap()
+        .try_as::<BigInt>()
+        .unwrap()
+        .words()
+        .as_ptr();
+
+    // A BigInt callback also accepts inline integers, mirroring the C++ cast 
rule.
+    let seen = std::cell::RefCell::new(Vec::new());
+    assert!(structural_visit(
+        &root,
+        |value: BigInt, _visitor: &mut VisitContext<'_, ()>| {
+            seen.borrow_mut().push(value);
+        }
+    )
+    .unwrap()
+    .is_none());
+    assert_eq!(seen.into_inner(), vec![BigInt::from(1i64), wide()]);
+
+    // The heap integer is a leaf: mapping the inline integer leaves it 
untouched.
+    let mapped = structural_map(
+        root.clone(),
+        |integer: i64| Any::from(integer + 1),
+        WalkOrder::PostOrder,
+    )
+    .and_then(Array::<Any>::try_from)
+    .unwrap();
+    assert_eq!(mapped.get(0).unwrap().try_as::<i64>(), Some(2));
+    let leaf = mapped.get(1).unwrap().try_as::<BigInt>().unwrap();
+    assert_eq!(leaf, wide());
+    assert_eq!(leaf.words().as_ptr(), wide_pointer);
+}
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 05449b8c..789e9b0a 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -188,6 +188,7 @@ def _render_type(schema: TypeSchema) -> tuple[str | None, 
RustImports]:
 def test_render_rust_type_value_positions() -> None:
     assert _render_type(TypeSchema("int"))[0] == "i64"
     assert _render_type(TypeSchema("str"))[0] == "String"
+    assert _render_type(TypeSchema("ffi.BigInt"))[0] == "BigInt"
     assert _render_type(TypeSchema("Any"))[0] == "Any"
     assert _render_type(TypeSchema("Callable", (TypeSchema("int"),)))[0] == 
"Function"
     assert _render_type(TypeSchema("Optional", (TypeSchema("str"),)))[0] == 
"Option<String>"

Reply via email to