Skip to content

Commit 839b405

Browse files
chore: Factor out bignat library (#12)
* chore: Factor out bignat library * Update CI * Add workspace metadata * Remove extension trait
1 parent beabc40 commit 839b405

8 files changed

Lines changed: 248 additions & 194 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ jobs:
2929
- name: Check clippy warnings
3030
run: cargo xclippy
3131
- name: Check *everything* compiles
32-
run: cargo check --all-targets --all-features
32+
run: cargo check --workspace --all-targets --all-features
33+
- name: Run workspace tests
34+
run: cargo test --workspace --all-targets --all-features
3335
# Restore and then save .lake to the GitHub cache
3436
# Essentially the same as lean-action but without re-downloading the Lean toolchain
3537
- uses: actions/cache@v5

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
[package]
2-
name = "lean-ffi"
1+
[workspace]
2+
members = ["bignat"]
3+
4+
[workspace.package]
35
version = "0.1.0"
46
edition = "2024"
7+
license = "MIT OR Apache-2.0"
8+
9+
[workspace.dependencies]
10+
num-bigint = "0.4.6"
11+
12+
[package]
13+
name = "lean-ffi"
14+
version.workspace = true
15+
edition.workspace = true
16+
license.workspace = true
517

618
[lib]
719
crate-type = ["lib", "staticlib"]
@@ -10,7 +22,8 @@ crate-type = ["lib", "staticlib"]
1022
test-ffi = []
1123

1224
[dependencies]
13-
num-bigint = "0.4.6"
25+
bignat = { path = "bignat" }
26+
num-bigint.workspace = true
1427

1528
[build-dependencies]
1629
bindgen = "0.72"
@@ -20,4 +33,4 @@ cc = "1"
2033
panic = "abort"
2134

2235
[profile.release]
23-
panic = "abort"
36+
panic = "abort"

bignat/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "bignat"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
7+
[dependencies]
8+
num-bigint.workspace = true

bignat/src/lib.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! Arbitrary-precision natural number, wrapping `BigUint`.
2+
//!
3+
//! This crate holds the generic `Nat` type used across projects that need a
4+
//! `BigUint`-shaped natural number without taking a dependency on Lean. The
5+
//! `lean-ffi` crate re-exports this `Nat`; the corresponding Lean-side
6+
//! decode/encode lives there as inherent methods on `LeanNat<LeanOwned>`
7+
//! (`from_nat` / `to_nat`).
8+
9+
use std::fmt;
10+
11+
use num_bigint::BigUint;
12+
13+
#[derive(Hash, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
14+
pub struct Nat(pub BigUint);
15+
16+
impl fmt::Display for Nat {
17+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+
write!(f, "{}", self.0)
19+
}
20+
}
21+
22+
impl From<u64> for Nat {
23+
fn from(x: u64) -> Self {
24+
Nat(BigUint::from(x))
25+
}
26+
}
27+
28+
impl Nat {
29+
pub const ZERO: Self = Self(BigUint::ZERO);
30+
31+
/// Try to convert to u64, returning None if the value is too large.
32+
#[inline]
33+
pub fn to_u64(&self) -> Option<u64> {
34+
u64::try_from(&self.0).ok()
35+
}
36+
37+
#[inline]
38+
pub fn from_le_bytes(bytes: &[u8]) -> Nat {
39+
Nat(BigUint::from_bytes_le(bytes))
40+
}
41+
42+
#[inline]
43+
pub fn to_le_bytes(&self) -> Vec<u8> {
44+
self.0.to_bytes_le()
45+
}
46+
}

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
pkgs.libiconv
6969
];
7070
};
71-
rustPkg = craneLib.buildPackage (craneArgs // {cargoExtraArgs = "--locked";});
72-
rustPkgTest = craneLib.buildPackage (craneArgs // {cargoExtraArgs = "--locked --features test-ffi";});
71+
rustPkg = craneLib.buildPackage (craneArgs // {cargoExtraArgs = "--locked --workspace";});
72+
rustPkgTest = craneLib.buildPackage (craneArgs // {cargoExtraArgs = "--locked -p lean-ffi --features test-ffi";});
7373

7474
# Lake test package
7575
lake2nix = pkgs.callPackage lean4-nix.lake {};

src/nat.rs

Lines changed: 50 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,50 @@
1-
//! Lean `Nat` (arbitrary-precision natural number) representation.
1+
//! Lean `Nat` (arbitrary-precision natural number) FFI surface.
2+
//!
3+
//! The generic `Nat = BigUint` newtype lives in the `bignat` crate and is
4+
//! re-exported here. This module adds the Lean-side encode/decode operations
5+
//! as inherent methods on [`LeanNat<LeanOwned>`], plus the GMP-backed limb
6+
//! constructor used to build big Nats.
27
//!
38
//! Lean stores small naturals as tagged scalars and large ones as GMP
4-
//! `mpz_object`s on the heap. This module handles both representations.
9+
//! `mpz_object`s on the heap; both representations are handled here.
510
611
use std::ffi::c_int;
7-
use std::fmt;
812
use std::mem::MaybeUninit;
913

1014
use num_bigint::BigUint;
1115

12-
use crate::object::{LeanNat, LeanOwned, LeanRef};
13-
14-
/// Arbitrary-precision natural number, wrapping `BigUint`.
15-
#[derive(Hash, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
16-
pub struct Nat(pub BigUint);
16+
pub use bignat::Nat;
1717

18-
impl fmt::Display for Nat {
19-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20-
write!(f, "{}", self.0)
21-
}
22-
}
23-
24-
impl From<u64> for Nat {
25-
fn from(x: u64) -> Self {
26-
Nat(BigUint::from(x))
27-
}
28-
}
29-
30-
impl Nat {
31-
pub const ZERO: Self = Self(BigUint::ZERO);
32-
33-
/// Try to convert to u64, returning None if the value is too large.
34-
#[inline]
35-
pub fn to_u64(&self) -> Option<u64> {
36-
u64::try_from(&self.0).ok()
37-
}
18+
use crate::include::lean_uint64_to_nat;
19+
use crate::object::{LeanNat, LeanOwned, LeanRef};
3820

39-
/// Decode a `Nat` from any Lean reference. Handles both scalar (unboxed)
21+
impl LeanNat<LeanOwned> {
22+
/// Decode a [`Nat`] from any Lean reference. Handles both scalar (unboxed)
4023
/// and heap-allocated (GMP `mpz_object`) representations.
41-
pub fn from_obj(obj: &impl LeanRef) -> Nat {
24+
pub fn to_nat(obj: &impl LeanRef) -> Nat {
4225
if obj.is_scalar() {
4326
Nat(BigUint::from(obj.unbox_usize() as u64))
4427
} else {
45-
// Heap-allocated big integer (mpz_object)
4628
let mpz: &MpzObject = unsafe { &*obj.as_raw().cast() };
4729
Nat(mpz.m_value.to_biguint())
4830
}
4931
}
5032

51-
#[inline]
52-
pub fn from_le_bytes(bytes: &[u8]) -> Nat {
53-
Nat(BigUint::from_bytes_le(bytes))
54-
}
55-
56-
#[inline]
57-
pub fn to_le_bytes(&self) -> Vec<u8> {
58-
self.0.to_bytes_le()
59-
}
60-
61-
/// Convert this `Nat` into a Lean `Nat` (returns an owned reference).
62-
pub fn to_lean(&self) -> LeanNat<LeanOwned> {
63-
// Try to get as u64 first
64-
if let Some(val) = self.to_u64() {
65-
// For small values that fit in a boxed scalar (max value is usize::MAX >> 1)
66-
if val <= (usize::MAX >> 1) as u64 {
33+
/// Convert a [`Nat`] into a Lean `Nat` (owned reference).
34+
pub fn from_nat(n: &Nat) -> Self {
35+
let raw = match n.to_u64() {
36+
Some(val) if val <= (usize::MAX >> 1) as u64 => {
6737
#[allow(clippy::cast_possible_truncation)]
68-
return LeanNat::new(LeanOwned::box_usize(val as usize));
38+
let scalar = val as usize;
39+
LeanOwned::box_usize(scalar)
6940
}
70-
return LeanNat::new(LeanOwned::from_nat_u64(val));
71-
}
72-
// For values larger than u64, access limbs directly (no byte round-trip)
73-
let limbs = self.0.to_u64_digits();
74-
LeanNat::new(unsafe { lean_nat_from_limbs(limbs.len(), limbs.as_ptr()) })
41+
Some(val) => LeanOwned::from_nat_u64(val),
42+
None => {
43+
let limbs = n.0.to_u64_digits();
44+
unsafe { lean_nat_from_limbs(limbs.len(), limbs.as_ptr()) }
45+
}
46+
};
47+
LeanNat::new(raw)
7548
}
7649
}
7750

@@ -113,8 +86,6 @@ impl Mpz {
11386
// GMP interop for building Lean Nat objects from limbs
11487
// =============================================================================
11588

116-
use crate::include::lean_uint64_to_nat;
117-
11889
/// LEAN_MAX_SMALL_NAT = SIZE_MAX >> 1
11990
const LEAN_MAX_SMALL_NAT: u64 = (usize::MAX >> 1) as u64;
12091

@@ -145,27 +116,29 @@ unsafe extern "C" {
145116
/// # Safety
146117
/// `limbs` must be valid for reading `num_limbs` elements.
147118
pub unsafe fn lean_nat_from_limbs(num_limbs: usize, limbs: *const u64) -> LeanOwned {
148-
if num_limbs == 0 {
149-
return LeanOwned::box_usize(0);
150-
}
151-
let first = unsafe { *limbs };
152-
if num_limbs == 1 && first <= LEAN_MAX_SMALL_NAT {
153-
#[allow(clippy::cast_possible_truncation)] // only targets 64-bit
154-
return LeanOwned::box_usize(first as usize);
155-
}
156-
if num_limbs == 1 {
157-
return unsafe { LeanOwned::from_raw(lean_uint64_to_nat(first)) };
158-
}
159-
// Multi-limb: use GMP
160-
unsafe {
161-
let mut value = MaybeUninit::<Mpz>::uninit();
162-
mpz_init(value.as_mut_ptr());
163-
// order = -1 (least significant limb first)
164-
// size = 8 bytes per limb, endian = 0 (native), nails = 0
165-
mpz_import(value.as_mut_ptr(), num_limbs, -1, 8, 0, 0, limbs);
166-
// lean_alloc_mpz deep-copies; we must free the original
167-
let result = lean_alloc_mpz(value.as_mut_ptr());
168-
mpz_clear(value.as_mut_ptr());
169-
LeanOwned::from_raw(result.cast())
119+
match num_limbs {
120+
0 => LeanOwned::box_usize(0),
121+
1 => {
122+
let first = unsafe { *limbs };
123+
if first <= LEAN_MAX_SMALL_NAT {
124+
#[allow(clippy::cast_possible_truncation)] // only targets 64-bit
125+
let scalar = first as usize;
126+
LeanOwned::box_usize(scalar)
127+
} else {
128+
unsafe { LeanOwned::from_raw(lean_uint64_to_nat(first)) }
129+
}
130+
}
131+
// Multi-limb: use GMP
132+
_ => unsafe {
133+
let mut value = MaybeUninit::<Mpz>::uninit();
134+
mpz_init(value.as_mut_ptr());
135+
// order = -1 (least significant limb first)
136+
// size = 8 bytes per limb, endian = 0 (native), nails = 0
137+
mpz_import(value.as_mut_ptr(), num_limbs, -1, 8, 0, 0, limbs);
138+
// lean_alloc_mpz deep-copies; we must free the original
139+
let result = lean_alloc_mpz(value.as_mut_ptr());
140+
mpz_clear(value.as_mut_ptr());
141+
LeanOwned::from_raw(result.cast())
142+
},
170143
}
171144
}

0 commit comments

Comments
 (0)