diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index fdf6b999..031cea57 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -45,6 +45,14 @@ jobs: python -m pip install maturin rustup target add x86_64-unknown-linux-musl + - name: Install GMP + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get install lzip -y + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.lz | lzip -dc | tar x && \ + cd gmp-6.2.1 && ./configure --enable-fat --with-pic && \ + make -j 6 && sudo make install && cd .. && rm -rf gmp-6.2.1 + - name: Build Windows if: startsWith(matrix.os, 'windows') run: | @@ -54,6 +62,7 @@ jobs: git clone https://github.com/Chia-Network/clvm_tools.git --branch=main --single-branch python -m pip install ./clvm_tools python -m pip install colorama + git clone https://github.com/Chia-Network/mpir_gc_x64.git --depth 1 maturin develop --release - name: Run benchmarks (Windows) @@ -62,6 +71,13 @@ jobs: . .\venv\Scripts\Activate.ps1 python benchmark/run-benchmark.py + - name: Install GMP + if: ${{ startsWith(matrix.os, 'ubuntu') }} + run: | + sudo apt install libgmp3-dev + ls -la /usr/lib64 + ls -la /usr/lib + - name: Build if: ${{ !startsWith(matrix.os, 'windows') }} env: @@ -106,6 +122,10 @@ jobs: - name: Install dependencies run: | + sudo apt-get install lzip -y + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.lz | lzip -dc | tar x && \ + cd gmp-6.2.1 && ./configure --enable-fat --with-pic && \ + make -j 6 && sudo make install && cd .. && rm -rf gmp-6.2.1 python -m pip install maturin rustup target add x86_64-unknown-linux-musl diff --git a/.github/workflows/build-arm64-wheels.yml b/.github/workflows/build-arm64-wheels.yml index cb40a22f..6172f47d 100644 --- a/.github/workflows/build-arm64-wheels.yml +++ b/.github/workflows/build-arm64-wheels.yml @@ -20,6 +20,9 @@ jobs: os: [ ARM64 ] # , ubuntu-latest ] steps: + + - uses: Chia-Network/actions/clean-workspace@main + - name: Checkout repository uses: actions/checkout@v2 with: @@ -41,7 +44,10 @@ jobs: echo $PATH && \ curl -L https://sh.rustup.rs > rustup-init.sh && \ sh rustup-init.sh -y && \ - yum -y install openssl-devel && \ + yum -y install sudo openssl-devel && \ + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.bz2 | tar -xj && \ + cd gmp-6.2.1 && ./configure --enable-fat --with-pic && \ + make -j 6 && sudo make install && cd .. && rm -rf gmp-6.2.1 && \ source $HOME/.cargo/env && \ rustup target add aarch64-unknown-linux-musl && \ rm -rf venv && \ diff --git a/.github/workflows/build-crate-and-npm.yml b/.github/workflows/build-crate-and-npm.yml index 54ab1330..2fcb58ad 100644 --- a/.github/workflows/build-crate-and-npm.yml +++ b/.github/workflows/build-crate-and-npm.yml @@ -23,6 +23,13 @@ jobs: with: fetch-depth: 0 + - name: Install GMP + run: | + sudo apt-get install lzip -y + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.lz | lzip -dc | tar x && \ + cd gmp-6.2.1 && ./configure --enable-fat --with-pic && \ + make -j 6 && sudo make install && cd .. && rm -rf gmp-6.2.1 + - name: Set up rusts uses: actions-rs/toolchain@v1 with: @@ -39,6 +46,8 @@ jobs: run: cargo +stable fmt -- --files-with-diff --check - name: clippy (stable) run: cargo +stable clippy + - name: install GMP + run: sudo apt install libgmp3-dev - name: tests run: cargo test && cargo test --release - name: build diff --git a/.github/workflows/build-m1-wheel.yml b/.github/workflows/build-m1-wheel.yml index 941d538e..b3b91b6d 100644 --- a/.github/workflows/build-m1-wheel.yml +++ b/.github/workflows/build-m1-wheel.yml @@ -37,13 +37,23 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rust.sh arch -arm64 sh rust.sh -y + - name: Install GMP + run: | + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.lz | tar x + cd gmp-6.2.1 + ./configure --enable-fat --with-pic + make -j 6 + sudo make install + cd .. + rm -rf gmp-6.2.1 + - name: Build m1 wheels run: | arch -arm64 python3 -m venv venv . ./venv/bin/activate export PATH=~/.cargo/bin:$PATH arch -arm64 pip install maturin - arch -arm64 maturin build --no-sdist -i python --release --strip --cargo-extra-args="--features=openssl" + arch -arm64 maturin build --no-sdist --release --strip --cargo-extra-args="--features=openssl" - name: Install clvm_rs wheel run: | diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9dee995c..45432e11 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -47,6 +47,7 @@ jobs: - name: Build MacOs with maturin on Python ${{ matrix.python }} if: startsWith(matrix.os, 'macos') run: | + brew install gmp python -m venv venv ln -s venv/bin/activate . ./activate @@ -69,7 +70,10 @@ jobs: bash -exc '\ curl -L https://sh.rustup.rs > rustup-init.sh && \ sh rustup-init.sh -y && \ - yum -y install openssl-devel && \ + yum -y install sudo openssl-devel && \ + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.bz2 | tar -xj && \ + cd gmp-6.2.1 && ./configure --enable-fat --with-pic && \ + make -j 6 && sudo make install && cd .. && rm -rf gmp-6.2.1 && \ source $HOME/.cargo/env && \ rustup target add x86_64-unknown-linux-musl && \ rm -rf venv && \ @@ -95,6 +99,7 @@ jobs: python -m venv venv . .\venv\Scripts\Activate.ps1 ln -s venv\Scripts\Activate.ps1 activate + git clone https://github.com/Chia-Network/mpir_gc_x64.git --depth 1 maturin build --no-sdist -i python --release --strip # this will install into the venv # it'd be better to use the wheel, but I can't figure out how to do that @@ -227,6 +232,8 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: nightly + - name: install GMP + run: sudo apt install libgmp3-dev - name: cargo-fuzz run: cargo +nightly install cargo-fuzz - name: build @@ -239,10 +246,22 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 1 + - name: Install GMP + run: | + sudo apt-get install lzip -y + curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.lz | lzip -dc | tar x + cd gmp-6.2.1 + ./configure --enable-fat --with-pic + make -j 6 + sudo make install + cd .. + rm -rf gmp-6.2.1 - name: Install rust uses: actions-rs/toolchain@v1 with: toolchain: stable components: rustfmt, clippy + - name: install GMP + run: sudo apt install libgmp3-dev - name: cargo test run: cargo test diff --git a/Cargo.lock b/Cargo.lock index 3aa5bf68..10f6944c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,9 +85,6 @@ dependencies = [ "bls12_381", "hex", "lazy_static", - "num-bigint", - "num-integer", - "num-traits", "openssl", "pyo3", "sha2", @@ -254,36 +251,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "num-bigint" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index 24d25a14..6d0ad4a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,6 @@ default = ["extension-module"] [dependencies] hex = "=0.4.3" lazy_static = "=1.4.0" -num-bigint = "=0.4.0" -num-traits = "=0.2.14" -num-integer = "=0.1.44" bls12_381 = "=0.5.0" sha2 = "=0.9.5" openssl = { version = "0.10.35", features = ["vendored"], optional = true } diff --git a/README.md b/README.md index c40ede7f..c0238e6a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ Use `maturin` to build the python interface. First, install into current virtual $ pip install maturin ``` +As we need `MPIR` for MSVC builds, prepare this dependency with + +``` +$ git clone https://github.com/Chia-Network/mpir_gc_x64.git --depth 1 +``` + Build `clvm_rs` directly into the current virtualenv with ``` diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..e84daf04 --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +fn main() { + if cfg!(windows) { + println!("cargo:rustc-link-lib=mpir"); + println!("cargo:rustc-link-search=mpir_gc_x64"); + } else { + println!("cargo:rustc-link-lib=gmp"); + println!("cargo:rustc-link-search=/usr/local/lib"); + println!("cargo:rustc-link-search=/opt/homebrew/lib"); + } +} diff --git a/src/gen/conditions.rs b/src/gen/conditions.rs index cc1ae6f8..370f064b 100644 --- a/src/gen/conditions.rs +++ b/src/gen/conditions.rs @@ -555,8 +555,6 @@ use crate::serialize::node_to_bytes; #[cfg(test)] use hex::FromHex; #[cfg(test)] -use num_traits::Num; -#[cfg(test)] use std::collections::HashMap; #[cfg(test)] @@ -689,7 +687,7 @@ fn parse_list_impl( (a.new_atom(&buf).unwrap(), v.len() + 1) } else if input.starts_with("-") || "0123456789".contains(input.get(0..1).unwrap()) { let v = input.split_once(" ").unwrap().0; - let num = Number::from_str_radix(v, 10).unwrap(); + let num = Number::from_str_radix(v, 10); (ptr_from_number(a, &num).unwrap(), v.len() + 1) } else { panic!("atom not supported \"{}\"", input); diff --git a/src/gmp_ffi.rs b/src/gmp_ffi.rs new file mode 100644 index 00000000..8f6736d9 --- /dev/null +++ b/src/gmp_ffi.rs @@ -0,0 +1,118 @@ +#![allow(non_camel_case_types, non_snake_case)] + +use core::ptr::NonNull; +use std::ffi::c_void; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct mpz_t { + pub alloc: c_int, + pub size: c_int, + pub d: NonNull, +} + +type c_int = i32; +type c_long = i64; +type c_ulong = u64; +type c_ulonglong = u64; +type mpz_srcptr = *const mpz_t; +type mpz_ptr = *mut mpz_t; +type bitcnt_t = c_ulong; + +extern "C" { + #[link_name = "__gmpz_init"] + pub fn mpz_init(x: mpz_ptr); + #[link_name = "__gmpz_import"] + pub fn mpz_import( + rop: mpz_ptr, + count: usize, + order: c_int, + size: usize, + endian: c_int, + nails: usize, + op: *const c_void, + ); + #[link_name = "__gmpz_add_ui"] + pub fn mpz_add_ui(rop: mpz_ptr, op1: mpz_srcptr, op2: c_ulong); + #[link_name = "__gmpz_set"] + pub fn mpz_set(rop: mpz_ptr, op: mpz_srcptr); + #[link_name = "__gmpz_export"] + pub fn mpz_export( + rop: *mut c_void, + countp: *mut usize, + order: c_int, + size: usize, + endian: c_int, + nails: usize, + op: mpz_srcptr, + ) -> *mut c_void; + #[link_name = "__gmpz_sizeinbase"] + pub fn mpz_sizeinbase(arg1: mpz_srcptr, arg2: c_int) -> usize; + #[link_name = "__gmpz_fdiv_qr"] + pub fn mpz_fdiv_qr(q: mpz_ptr, r: mpz_ptr, n: mpz_srcptr, d: mpz_srcptr); + #[link_name = "__gmpz_fdiv_q"] + pub fn mpz_fdiv_q(q: mpz_ptr, n: mpz_srcptr, d: mpz_srcptr); + #[link_name = "__gmpz_fdiv_r"] + pub fn mpz_fdiv_r(r: mpz_ptr, n: mpz_srcptr, d: mpz_srcptr); + #[link_name = "__gmpz_fdiv_q_2exp"] + pub fn mpz_fdiv_q_2exp(q: mpz_ptr, n: mpz_srcptr, b: bitcnt_t); + #[link_name = "__gmpz_init_set_ui"] + pub fn mpz_init_set_ui(rop: mpz_ptr, op: c_ulong); + #[link_name = "__gmpz_init_set_si"] + pub fn mpz_init_set_si(rop: mpz_ptr, op: c_long); + #[link_name = "__gmpz_clear"] + pub fn mpz_clear(x: mpz_ptr); + #[link_name = "__gmpz_add"] + pub fn mpz_add(rop: mpz_ptr, op1: mpz_srcptr, op2: mpz_srcptr); + #[link_name = "__gmpz_sub"] + pub fn mpz_sub(rop: mpz_ptr, op1: mpz_srcptr, op2: mpz_srcptr); + #[link_name = "__gmpz_mul"] + pub fn mpz_mul(rop: mpz_ptr, op1: mpz_srcptr, op2: mpz_srcptr); + #[link_name = "__gmpz_mul_2exp"] + pub fn mpz_mul_2exp(rop: mpz_ptr, op1: mpz_srcptr, op2: bitcnt_t); + #[link_name = "__gmpz_get_si"] + pub fn mpz_get_si(op: mpz_srcptr) -> c_long; + #[link_name = "__gmpz_and"] + pub fn mpz_and(rop: mpz_ptr, op1: mpz_srcptr, op2: mpz_srcptr); + #[link_name = "__gmpz_ior"] + pub fn mpz_ior(rop: mpz_ptr, op1: mpz_srcptr, op2: mpz_srcptr); + #[link_name = "__gmpz_xor"] + pub fn mpz_xor(rop: mpz_ptr, op1: mpz_srcptr, op2: mpz_srcptr); + #[link_name = "__gmpz_com"] + pub fn mpz_com(rop: mpz_ptr, op: mpz_srcptr); + #[link_name = "__gmpz_cmp"] + pub fn mpz_cmp(op1: mpz_srcptr, op2: mpz_srcptr) -> c_int; + #[link_name = "__gmpz_cmp_si"] + pub fn mpz_cmp_si(op1: mpz_srcptr, op2: c_long) -> c_int; + #[link_name = "__gmpz_cmp_ui"] + pub fn mpz_cmp_ui(op1: mpz_srcptr, op2: c_ulong) -> c_int; +} + +#[cfg(test)] +type c_char = i8; + +#[cfg(test)] +extern "C" { + #[link_name = "__gmpz_init_set_str"] + pub fn mpz_init_set_str(rop: mpz_ptr, str: *const c_char, base: c_int) -> c_int; + #[link_name = "__gmpz_get_str"] + pub fn mpz_get_str(str: *mut c_char, base: c_int, op: mpz_srcptr) -> *mut c_char; +} + +#[inline] +pub unsafe extern "C" fn mpz_neg(rop: mpz_ptr, op: mpz_srcptr) { + if rop as mpz_srcptr != op { + mpz_set(rop, op); + } + (*rop).size = -(*rop).size; +} + +#[inline] +pub unsafe extern "C" fn mpz_get_ui(op: mpz_srcptr) -> c_ulong { + if { (*op).size } != 0 { + let p = (*op).d.as_ptr(); + (*p) as c_ulong + } else { + 0 + } +} diff --git a/src/lib.rs b/src/lib.rs index f3a629e3..ba65cbab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ mod dialect; mod err_utils; pub mod f_table; mod gen; +mod gmp_ffi; mod int_to_bytes; pub mod more_ops; pub mod node; diff --git a/src/more_ops.rs b/src/more_ops.rs index 740664cd..7c556afd 100644 --- a/src/more_ops.rs +++ b/src/more_ops.rs @@ -1,7 +1,4 @@ use bls12_381::{G1Affine, G1Projective, Scalar}; -use num_bigint::{BigUint, Sign}; -use num_integer::Integer; -use std::convert::TryFrom; use std::ops::BitAndAssign; use std::ops::BitOrAssign; use std::ops::BitXorAssign; @@ -12,7 +9,7 @@ use crate::allocator::{Allocator, NodePtr, SExp}; use crate::cost::{check_cost, Cost}; use crate::err_utils::err; use crate::node::Node; -use crate::number::{number_from_u8, ptr_from_number, Number}; +use crate::number::{number_from_u8, ptr_from_number, Number, Sign}; use crate::op_utils::{ arg_count, atom, check_arg_count, i32_atom, int_atom, two_ints, u32_from_u8, }; @@ -354,7 +351,7 @@ pub fn op_sha256(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response pub fn op_add(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response { let mut cost = ARITH_BASE_COST; let mut byte_count: usize = 0; - let mut total: Number = 0.into(); + let mut total = Number::zero(); for arg in Node::new(a, input) { cost += ARITH_COST_PER_ARG; check_cost( @@ -365,7 +362,7 @@ pub fn op_add(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response { let blob = int_atom(&arg, "+")?; let v: Number = number_from_u8(blob); byte_count += blob.len(); - total += v; + total += &v; } let total = ptr_from_number(a, &total)?; cost += byte_count as Cost * ARITH_COST_PER_BYTE; @@ -375,7 +372,7 @@ pub fn op_add(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response { pub fn op_subtract(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response { let mut cost = ARITH_BASE_COST; let mut byte_count: usize = 0; - let mut total: Number = 0.into(); + let mut total = Number::zero(); let mut is_first = true; for arg in Node::new(a, input) { cost += ARITH_COST_PER_ARG; @@ -384,9 +381,9 @@ pub fn op_subtract(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Respons let v: Number = number_from_u8(blob); byte_count += blob.len(); if is_first { - total += v; + total += &v; } else { - total -= v; + total -= &v; }; is_first = false; } @@ -434,7 +431,7 @@ pub fn op_div(a: &mut Allocator, input: NodePtr, _max_cost: Cost) -> Response { // this is to preserve a buggy behavior from the initial implementation // of this operator. - if q == (-1).into() && r != 0.into() { + if q == -1 && r != 0 { q += 1; } let q1 = ptr_from_number(a, &q)?; @@ -641,7 +638,7 @@ pub fn op_lsh(a: &mut Allocator, input: NodePtr, _max_cost: Cost) -> Response { check_arg_count(&args, 2, "lsh")?; let a0 = args.first()?; let b0 = int_atom(&a0, "lsh")?; - let i0 = BigUint::from_bytes_be(b0); + let i0 = Number::from_unsigned_bytes_be(b0); let l0 = b0.len(); let rest = args.rest()?; let a1 = i32_atom(&rest.first()?, "lsh")?; @@ -649,8 +646,6 @@ pub fn op_lsh(a: &mut Allocator, input: NodePtr, _max_cost: Cost) -> Response { return args.rest()?.first()?.err("shift too large"); } - let i0: Number = i0.into(); - let v: Number = if a1 > 0 { i0 << a1 } else { i0 >> -a1 }; let l1 = limbs_for_int(&v); @@ -739,7 +734,7 @@ fn logior_op(a: &mut Number, b: &Number) { } pub fn op_logior(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response { - let v: Number = (0).into(); + let v = Number::zero(); binop_reduction("logior", a, v, input, max_cost, logior_op) } @@ -748,7 +743,7 @@ fn logxor_op(a: &mut Number, b: &Number) { } pub fn op_logxor(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response { - let v: Number = (0).into(); + let v = Number::zero(); binop_reduction("logxor", a, v, input, max_cost, logxor_op) } @@ -804,10 +799,10 @@ pub fn op_softfork(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Respons Some((p1, _)) => { let n: Number = number_from_u8(int_atom(&p1, "softfork")?); if n.sign() == Sign::Plus { - if n > Number::from(max_cost) { + if n > max_cost { return err(a.null(), "cost exceeded"); } - let cost: Cost = TryFrom::try_from(&n).unwrap(); + let cost: Cost = n.into(); Ok(Reduction(cost, args.null().node)) } else { args.err("cost must be > 0") @@ -824,14 +819,13 @@ lazy_static! { 0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, ]; - let n = BigUint::from_bytes_be(order_as_bytes); - n.into() + Number::from_unsigned_bytes_be(order_as_bytes) }; } fn mod_group_order(n: Number) -> Number { - let order = GROUP_ORDER.clone(); - let mut remainder = n.mod_floor(&order); + let order: &Number = &GROUP_ORDER; + let mut remainder = n.mod_floor(order); if remainder.sign() == Sign::Minus { remainder += order; } diff --git a/src/number.rs b/src/number.rs index b66a5df1..bd1f3076 100644 --- a/src/number.rs +++ b/src/number.rs @@ -1,22 +1,421 @@ use crate::allocator::{Allocator, NodePtr}; +use crate::gmp_ffi as gmp; use crate::node::Node; use crate::reduction::EvalErr; +use core::mem::MaybeUninit; +use std::cmp::Ordering; +use std::cmp::PartialOrd; +use std::ffi::c_void; +use std::ops::Drop; +use std::ops::{ + AddAssign, BitAndAssign, BitOrAssign, BitXorAssign, MulAssign, Not, Shl, Shr, SubAssign, +}; + +#[allow(clippy::enum_variant_names)] +#[derive(PartialEq)] +pub enum Sign { + Minus, + NoSign, + Plus, +} -use num_bigint::BigInt; -pub type Number = BigInt; +pub struct Number { + v: gmp::mpz_t, +} -pub fn ptr_from_number(allocator: &mut Allocator, item: &Number) -> Result { - let bytes: Vec = item.to_signed_bytes_be(); - let mut slice = bytes.as_slice(); +impl Number { + pub fn zero() -> Number { + let mut v = MaybeUninit::::uninit(); + unsafe { + gmp::mpz_init(v.as_mut_ptr()); + } + Number { + v: unsafe { v.assume_init() }, + } + } + + pub fn from_signed_bytes_be(v: &[u8]) -> Number { + let mut ret = Number::zero(); + if v.is_empty() { + return ret; + } + // mpz_import() only reads unsigned values + let negative = (v[0] & 0x80) != 0; + + if negative { + // since the bytes we read are two's complement + // if the most significant bit was set, we need to + // convert the value to a negative one. We do this by flipping + // all bits, adding one and then negating it. + let mut v = v.to_vec(); + for digit in &mut v { + *digit = !*digit; + } + unsafe { + gmp::mpz_import(&mut ret.v, v.len(), 1, 1, 0, 0, v.as_ptr() as *const c_void); + gmp::mpz_add_ui(&mut ret.v, &ret.v, 1); + gmp::mpz_neg(&mut ret.v, &ret.v); + } + } else { + unsafe { + gmp::mpz_import(&mut ret.v, v.len(), 1, 1, 0, 0, v.as_ptr() as *const c_void); + } + } + ret + } + + pub fn from_unsigned_bytes_be(v: &[u8]) -> Number { + let mut ret = Number::zero(); + if !v.is_empty() { + unsafe { + gmp::mpz_import(&mut ret.v, v.len(), 1, 1, 0, 0, v.as_ptr() as *const c_void); + } + } + ret + } + + pub fn to_signed_bytes_be(&self) -> Vec { + let size = (self.bits() + 7) / 8; + let mut ret: Vec = Vec::new(); + if size == 0 { + return ret; + } + ret.resize(size + 1, 0); + let sign = self.sign(); + let mut out_size: usize = size; + unsafe { + gmp::mpz_export( + ret.as_mut_slice()[1..].as_mut_ptr() as *mut c_void, + &mut out_size, + 1, + 1, + 0, + 0, + &self.v, + ); + } + // apparently mpz_export prints 0 bytes to the buffer if the value is 0 + // hence the special case in the assert below. + assert!(out_size == ret.len() - 1); + if sign == Sign::Minus { + // If the value is negative, we need to convert it to two's + // complement. We can't do that in-place. + let mut carry = true; + for digit in &mut ret.iter_mut().rev() { + let res = (!*digit).overflowing_add(carry as u8); + *digit = res.0; + carry = res.1; + } + assert!(!carry); + assert!(ret[0] & 0x80 != 0); + if (ret[1] & 0x80) != 0 { + ret.remove(0); + } + } else if ret[1] & 0x80 == 0 { + ret.remove(0); + } + ret + } + + pub fn to_bytes_le(&self) -> (Sign, Vec) { + let sgn = self.sign(); + + let size = (self.bits() + 7) / 8; + let mut ret: Vec = Vec::new(); + if size == 0 { + return (Sign::NoSign, ret); + } + ret.resize(size, 0); + + let mut out_size: usize = size; + unsafe { + gmp::mpz_export( + ret.as_mut_ptr() as *mut c_void, + &mut out_size, + -1, + 1, + 0, + 0, + &self.v, + ); + } + assert_eq!(out_size, ret.len()); + (sgn, ret) + } + + pub fn bits(&self) -> usize { + // GnuMP says that any integer needs at least 1 bit to be represented. + // but we say 0 requires 0 bits + if self.sign() == Sign::NoSign { + 0 + } else { + unsafe { gmp::mpz_sizeinbase(&self.v, 2) } + } + } + + pub fn sign(&self) -> Sign { + match unsafe { gmp::mpz_cmp_si(&self.v, 0) } { + d if d < 0 => Sign::Minus, + d if d > 0 => Sign::Plus, + _ => Sign::NoSign, + } + } + + // returns the quotient and remained, from dividing self with denominator + pub fn div_mod_floor(&self, denominator: &Number) -> (Number, Number) { + let mut q = Number::zero(); + let mut r = Number::zero(); + unsafe { + gmp::mpz_fdiv_qr(&mut q.v, &mut r.v, &self.v, &denominator.v); + } + (q, r) + } + + pub fn mod_floor(&self, denominator: &Number) -> Number { + let mut r = Number::zero(); + unsafe { + gmp::mpz_fdiv_r(&mut r.v, &self.v, &denominator.v); + } + r + } + + pub fn div_floor(&self, denominator: &Number) -> Number { + let mut ret = Number::zero(); + unsafe { + gmp::mpz_fdiv_q(&mut ret.v, &self.v, &denominator.v); + } + ret + } +} + +impl Drop for Number { + fn drop(&mut self) { + unsafe { + gmp::mpz_clear(&mut self.v); + } + } +} + +// Addition + +impl AddAssign<&Number> for Number { + fn add_assign(&mut self, other: &Self) { + unsafe { + gmp::mpz_add(&mut self.v, &self.v, &other.v); + } + } +} + +// This is only here for op_div() +impl AddAssign for Number { + fn add_assign(&mut self, other: u64) { + unsafe { + gmp::mpz_add_ui(&mut self.v, &self.v, other); + } + } +} + +// Subtraction + +impl SubAssign<&Number> for Number { + fn sub_assign(&mut self, other: &Self) { + unsafe { + gmp::mpz_sub(&mut self.v, &self.v, &other.v); + } + } +} + +// Multiplication + +impl MulAssign for Number { + fn mul_assign(&mut self, other: Self) { + unsafe { + gmp::mpz_mul(&mut self.v, &self.v, &other.v); + } + } +} + +// Shift + +impl Shl for Number { + type Output = Self; + fn shl(mut self, n: i32) -> Self { + assert!(n >= 0); + unsafe { + gmp::mpz_mul_2exp(&mut self.v, &self.v, n as u64); + } + self + } +} + +impl Shr for Number { + type Output = Self; + fn shr(mut self, n: i32) -> Self { + assert!(n >= 0); + unsafe { + gmp::mpz_fdiv_q_2exp(&mut self.v, &self.v, n as u64); + } + self + } +} + +// Conversion + +impl From for Number { + fn from(other: i64) -> Self { + let mut v = MaybeUninit::::uninit(); + unsafe { + gmp::mpz_init_set_si(v.as_mut_ptr(), other); + } + Number { + v: unsafe { v.assume_init() }, + } + } +} + +impl From for Number { + fn from(other: i32) -> Self { + let mut v = MaybeUninit::::uninit(); + unsafe { + gmp::mpz_init_set_si(v.as_mut_ptr(), other as i64); + } + Number { + v: unsafe { v.assume_init() }, + } + } +} + +impl From for Number { + fn from(other: u64) -> Self { + let mut v = MaybeUninit::::uninit(); + unsafe { + gmp::mpz_init_set_ui(v.as_mut_ptr(), other); + } + Number { + v: unsafe { v.assume_init() }, + } + } +} + +impl From for Number { + fn from(other: usize) -> Self { + let mut v = MaybeUninit::::uninit(); + unsafe { + gmp::mpz_init_set_ui(v.as_mut_ptr(), other as u64); + } + Number { + v: unsafe { v.assume_init() }, + } + } +} - // make number minimal by removing leading zeros - while (!slice.is_empty()) && (slice[0] == 0) { - if slice.len() > 1 && (slice[1] & 0x80 == 0x80) { - break; +impl From for u64 { + fn from(n: Number) -> u64 { + unsafe { + assert!(gmp::mpz_sizeinbase(&n.v, 2) <= 64); + assert!(gmp::mpz_cmp_si(&n.v, 0) >= 0); + gmp::mpz_get_ui(&n.v) } - slice = &slice[1..]; } - allocator.new_atom(slice) +} + +impl From for i64 { + fn from(n: Number) -> i64 { + unsafe { + assert!(gmp::mpz_sizeinbase(&n.v, 2) <= 64); + gmp::mpz_get_si(&n.v) + } + } +} + +// Bit operations + +impl BitXorAssign<&Number> for Number { + fn bitxor_assign(&mut self, other: &Self) { + unsafe { + gmp::mpz_xor(&mut self.v, &self.v, &other.v); + } + } +} + +impl BitOrAssign<&Number> for Number { + fn bitor_assign(&mut self, other: &Self) { + unsafe { + gmp::mpz_ior(&mut self.v, &self.v, &other.v); + } + } +} + +impl BitAndAssign<&Number> for Number { + fn bitand_assign(&mut self, other: &Self) { + unsafe { + gmp::mpz_and(&mut self.v, &self.v, &other.v); + } + } +} + +impl Not for Number { + type Output = Self; + fn not(self) -> Self { + let mut ret = Number::zero(); + unsafe { + gmp::mpz_com(&mut ret.v, &self.v); + } + ret + } +} + +// Comparisons + +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + unsafe { gmp::mpz_cmp(&self.v, &other.v) == 0 } + } +} + +impl PartialEq for Number { + fn eq(&self, other: &u64) -> bool { + unsafe { gmp::mpz_cmp_ui(&self.v, *other) == 0 } + } +} + +impl PartialEq for Number { + fn eq(&self, other: &i64) -> bool { + unsafe { gmp::mpz_cmp_si(&self.v, *other) == 0 } + } +} + +impl PartialEq for Number { + fn eq(&self, other: &i32) -> bool { + unsafe { gmp::mpz_cmp_si(&self.v, *other as i64) == 0 } + } +} + +fn ord_helper(r: i32) -> Option { + match r { + d if d < 0 => Some(Ordering::Less), + d if d > 0 => Some(Ordering::Greater), + _ => Some(Ordering::Equal), + } +} + +impl PartialOrd for Number { + fn partial_cmp(&self, other: &Number) -> Option { + ord_helper(unsafe { gmp::mpz_cmp(&self.v, &other.v) }) + } +} + +impl PartialOrd for Number { + fn partial_cmp(&self, other: &u64) -> Option { + ord_helper(unsafe { gmp::mpz_cmp_ui(&self.v, *other) }) + } +} + +unsafe impl Sync for Number {} + +pub fn ptr_from_number(allocator: &mut Allocator, item: &Number) -> Result { + let bytes: Vec = item.to_signed_bytes_be(); + allocator.new_atom(bytes.as_slice()) } impl From<&Node<'_>> for Option { @@ -27,11 +426,48 @@ impl From<&Node<'_>> for Option { } pub fn number_from_u8(v: &[u8]) -> Number { - let len = v.len(); - if len == 0 { - 0.into() - } else { - Number::from_signed_bytes_be(v) + Number::from_signed_bytes_be(v) +} + +// ==== TESTS ==== + +#[cfg(test)] +use std::ffi::{CStr, CString}; +#[cfg(test)] +use std::fmt; + +#[cfg(test)] +impl Number { + pub fn from_str_radix(mut s: &str, radix: i32) -> Number { + let negative = s.get(0..1).unwrap() == "-"; + if negative { + s = s.get(1..).unwrap(); + } + let input = CString::new(s).unwrap(); + let mut v = MaybeUninit::::uninit(); + let result = unsafe { gmp::mpz_init_set_str(v.as_mut_ptr(), input.as_ptr(), radix) }; + // v will be initialized even if an error occurs, so we will need to + // capture it in a Number regardless + let mut ret = Number { + v: unsafe { v.assume_init() }, + }; + if negative { + unsafe { + gmp::mpz_neg(&mut ret.v, &ret.v); + } + } + assert!(result == 0); + ret + } +} + +#[cfg(test)] +impl fmt::Display for Number { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let len = unsafe { gmp::mpz_sizeinbase(&self.v, 10) } + 2; + let mut storage = Vec::::with_capacity(len); + let c_str = unsafe { gmp::mpz_get_str(storage.as_mut_ptr(), 10, &self.v) }; + unsafe { f.write_str(CStr::from_ptr(c_str).to_str().unwrap()) } } } @@ -95,12 +531,6 @@ fn test_ptr_from_number() { assert_eq!(&[0x40, 0x00], &a.atom(ptr)); } -#[cfg(test)] -use num_bigint::{BigUint, Sign}; - -#[cfg(test)] -use std::convert::TryFrom; - #[cfg(test)] fn roundtrip_bytes(b: &[u8]) { let negative = b.len() > 0 && (b[0] & 0x80) != 0; @@ -118,25 +548,12 @@ fn roundtrip_bytes(b: &[u8]) { } let round_trip = num.to_signed_bytes_be(); - // num-bigin produces a single 0 byte for the value 0. We expect an - // empty array - let round_trip = if round_trip == &[0] { - &round_trip[1..] - } else { - &round_trip - }; assert_eq!(round_trip, b); // test to_bytes_le() let (sign, mut buf_le) = num.to_bytes_le(); - // there's a special case for empty input buffers, which will result in - // a single 0 byte here - if b == &[] { - assert_eq!(buf_le, &[0]); - buf_le.remove(0); - } assert!(sign == num.sign()); // the buffer we get from to_bytes_le() is unsigned (since the sign is @@ -153,7 +570,10 @@ fn roundtrip_bytes(b: &[u8]) { if sign != Sign::Minus { assert!(buf_le.iter().eq(b.iter().rev())); } else { - let negated = -num; + let mut negated = Number::zero(); + unsafe { + gmp::mpz_neg(&mut negated.v, &num.v); + } let magnitude = negated.to_signed_bytes_be(); assert!(buf_le.iter().eq(magnitude.iter().rev())); } @@ -161,7 +581,7 @@ fn roundtrip_bytes(b: &[u8]) { // test parsing unsigned bytes { - let unsigned_num: Number = BigUint::from_bytes_be(b).into(); + let unsigned_num = Number::from_unsigned_bytes_be(b); assert!(unsigned_num.sign() != Sign::Minus); let unsigned_round_trip = unsigned_num.to_signed_bytes_be(); let unsigned_round_trip = if unsigned_round_trip == &[0] { @@ -183,6 +603,7 @@ fn roundtrip_bytes(b: &[u8]) { fn test_number_round_trip_bytes() { roundtrip_bytes(&[]); + // 0 doesn't round-trip, since we represent that by an empty buffer for i in 1..=255 { roundtrip_bytes(&[i]); } @@ -222,8 +643,32 @@ fn roundtrip_u64(v: u64) { assert!(num.sign() != Sign::Minus); assert!(num.bits() <= 64); + assert!(!(num < v)); + assert!(!(num > v)); + assert!(!(num != v)); + assert!(num == v); + assert!(num <= v); + assert!(num >= v); + + if v != u64::MAX { + assert!(num < v + 1); + assert!(!(num > v + 1)); + assert!(num != v + 1); + assert!(!(num == v + 1)); + assert!(num <= v + 1); + assert!(!(num >= v + 1)); + } - let round_trip: u64 = TryFrom::try_from(num).unwrap(); + if v != u64::MIN { + assert!(!(num < v - 1)); + assert!(num > v - 1); + assert!(num != v - 1); + assert!(!(num == v - 1)); + assert!(!(num <= v - 1)); + assert!(num >= v - 1); + } + + let round_trip: u64 = num.into(); assert_eq!(round_trip, v); } @@ -269,8 +714,7 @@ fn roundtrip_i64(v: i64) { } assert!(num.bits() <= 64); - - let round_trip: i64 = TryFrom::try_from(num).unwrap(); + let round_trip: i64 = num.into(); assert_eq!(round_trip, v); } @@ -323,7 +767,7 @@ fn test_round_trip_i64() { #[cfg(test)] fn bits(b: &[u8]) -> u64 { - Number::from_signed_bytes_be(b).bits() + Number::from_signed_bytes_be(b).bits() as u64 } #[test] diff --git a/src/test_ops.rs b/src/test_ops.rs index f722d19c..62af8c43 100644 --- a/src/test_ops.rs +++ b/src/test_ops.rs @@ -9,7 +9,6 @@ use crate::more_ops::{ use crate::number::{ptr_from_number, Number}; use crate::reduction::{Reduction, Response}; use hex::FromHex; -use num_traits::Num; use std::collections::HashMap; static TEST_CASES: &str = r#" @@ -705,7 +704,7 @@ fn parse_atom(a: &mut Allocator, v: &str) -> NodePtr { } if v.starts_with("-") || "0123456789".contains(v.get(0..1).unwrap()) { - let num = Number::from_str_radix(v, 10).unwrap(); + let num = Number::from_str_radix(v, 10); return ptr_from_number(a, &num).unwrap(); }