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>"