Skip to content

malachite#739

Merged
arvidn merged 2 commits intomainfrom
malachite-arithmetic
Mar 9, 2026
Merged

malachite#739
arvidn merged 2 commits intomainfrom
malachite-arithmetic

Conversation

@arvidn
Copy link
Contributor

@arvidn arvidn commented Mar 9, 2026

based on @Rigidity's and @wjblanke's work.

  • add malachite as a dependency
  • add support to Allocator to create and retrieve malachite large integers
  • add alternative implementations for some arithmetic operators where malachite has better performance than num-bigint
  • add ClvmFlags feature flag to select the malachite implementations (opt-in)
  • extend tests and fuzzers to cover the new malachite implementations

Note

Medium Risk
Touches core arithmetic op implementations and allocator number serialization, which can affect consensus-critical execution if enabled; risk is mitigated by being gated behind ClvmFlags::MALACHITE and reinforced with expanded fuzzing/tests.

Overview
Adds malachite-bigint as a new bigint backend and wires it into the VM as an opt-in execution mode.

This extends Allocator/op_utils with Malachite number encoding/decoding helpers, and adds parallel Malachite implementations for div, divmod, mod, and modpow (including the *_limit variants) while preserving existing cost accounting and error behavior.

ChiaDialect gains a new ClvmFlags::MALACHITE toggle to route opcodes 19/20/60/61 to the Malachite versions, and test coverage is expanded (unit tests, op-tests harness, and fuzz targets) to validate equivalence between the original and Malachite implementations.

Written by Cursor Bugbot for commit 55284b8. This will update automatically on new commits. Configure here.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Extensive duplicated logic across malachite and num-bigint implementations
    • I replaced duplicated num-bigint/malachite operation bodies with shared generic helpers so both variants now execute one common implementation path.

Create PR

Or push these changes by commenting:

@cursor push 8142c259a6
Preview (8142c259a6)
diff --git a/src/more_ops.rs b/src/more_ops.rs
--- a/src/more_ops.rs
+++ b/src/more_ops.rs
@@ -608,19 +608,42 @@
 }
 
 fn op_div_impl(a: &mut Allocator, input: NodePtr, max_cost: Cost, limit: bool) -> Response {
+    op_div_impl_inner(
+        a,
+        input,
+        max_cost,
+        limit,
+        int_atom,
+        |v: &Number| v.sign() == Sign::NoSign,
+        Allocator::new_number,
+    )
+}
+
+fn op_div_impl_inner<T>(
+    a: &mut Allocator,
+    input: NodePtr,
+    max_cost: Cost,
+    limit: bool,
+    int_atom_fn: fn(&Allocator, NodePtr, &str) -> crate::error::Result<(T, usize)>,
+    is_zero: fn(&T) -> bool,
+    new_number_fn: fn(&mut Allocator, T) -> crate::error::Result<NodePtr>,
+) -> Response
+where
+    T: Integer,
+{
     let [v0, v1] = get_args::<2>(a, input, "/")?;
-    let (a0, a0_len) = int_atom(a, v0, "/")?;
-    let (a1, a1_len) = int_atom(a, v1, "/")?;
+    let (a0, a0_len) = int_atom_fn(a, v0, "/")?;
+    let (a1, a1_len) = int_atom_fn(a, v1, "/")?;
     if limit && a0_len > 2048 {
         return Err(EvalErr::InvalidOpArg(input, "div".to_string()));
     }
     let cost = DIV_BASE_COST + ((a0_len + a1_len) as Cost) * DIV_COST_PER_BYTE;
     check_cost(cost, max_cost)?;
-    if a1.sign() == Sign::NoSign {
+    if is_zero(&a1) {
         return Err(EvalErr::DivisionByZero(input));
     }
     let q = a0.div_floor(&a1);
-    let q = a.new_number(q)?;
+    let q = new_number_fn(a, q)?;
     Ok(malloc_cost(a, cost, q))
 }
 
@@ -630,37 +653,55 @@
     max_cost: Cost,
     limit: bool,
 ) -> Response {
-    let [v0, v1] = get_args::<2>(a, input, "/")?;
-    let (a0, a0_len) = malachite_int_atom(a, v0, "/")?;
-    let (a1, a1_len) = malachite_int_atom(a, v1, "/")?;
-    if limit && a0_len > 2048 {
-        return Err(EvalErr::InvalidOpArg(input, "div".to_string()));
-    }
-    let cost = DIV_BASE_COST + ((a0_len + a1_len) as Cost) * DIV_COST_PER_BYTE;
-    check_cost(cost, max_cost)?;
-    if a1.sign() == malachite_bigint::Sign::NoSign {
-        return Err(EvalErr::DivisionByZero(input));
-    }
-    let q = a0.div_floor(&a1);
-    let q = a.new_malachite_number(q)?;
-    Ok(malloc_cost(a, cost, q))
+    op_div_impl_inner(
+        a,
+        input,
+        max_cost,
+        limit,
+        malachite_int_atom,
+        |v: &malachite_bigint::BigInt| v.sign() == malachite_bigint::Sign::NoSign,
+        Allocator::new_malachite_number,
+    )
 }
 
 fn op_divmod_impl(a: &mut Allocator, input: NodePtr, max_cost: Cost, limit: bool) -> Response {
+    op_divmod_impl_inner(
+        a,
+        input,
+        max_cost,
+        limit,
+        int_atom,
+        |v: &Number| v.sign() == Sign::NoSign,
+        Allocator::new_number,
+    )
+}
+
+fn op_divmod_impl_inner<T>(
+    a: &mut Allocator,
+    input: NodePtr,
+    max_cost: Cost,
+    limit: bool,
+    int_atom_fn: fn(&Allocator, NodePtr, &str) -> crate::error::Result<(T, usize)>,
+    is_zero: fn(&T) -> bool,
+    new_number_fn: fn(&mut Allocator, T) -> crate::error::Result<NodePtr>,
+) -> Response
+where
+    T: Integer,
+{
     let [v0, v1] = get_args::<2>(a, input, "divmod")?;
-    let (a0, a0_len) = int_atom(a, v0, "divmod")?;
-    let (a1, a1_len) = int_atom(a, v1, "divmod")?;
+    let (a0, a0_len) = int_atom_fn(a, v0, "divmod")?;
+    let (a1, a1_len) = int_atom_fn(a, v1, "divmod")?;
     if limit && a0_len > 2048 {
         return Err(EvalErr::InvalidOpArg(input, "divmod".to_string()));
     }
     let cost = DIVMOD_BASE_COST + ((a0_len + a1_len) as Cost) * DIVMOD_COST_PER_BYTE;
     check_cost(cost, max_cost)?;
-    if a1.sign() == Sign::NoSign {
+    if is_zero(&a1) {
         return Err(EvalErr::DivisionByZero(input));
     }
     let (q, r) = a0.div_mod_floor(&a1);
-    let q1 = a.new_number(q)?;
-    let r1 = a.new_number(r)?;
+    let q1 = new_number_fn(a, q)?;
+    let r1 = new_number_fn(a, r)?;
 
     let c = (a.atom_len(q1) + a.atom_len(r1)) as Cost * MALLOC_COST_PER_BYTE;
     let r: NodePtr = a.new_pair(q1, r1)?;
@@ -673,24 +714,15 @@
     max_cost: Cost,
     limit: bool,
 ) -> Response {
-    let [v0, v1] = get_args::<2>(a, input, "divmod")?;
-    let (a0, a0_len) = malachite_int_atom(a, v0, "divmod")?;
-    let (a1, a1_len) = malachite_int_atom(a, v1, "divmod")?;
-    if limit && a0_len > 2048 {
-        return Err(EvalErr::InvalidOpArg(input, "divmod".to_string()));
-    }
-    let cost = DIVMOD_BASE_COST + ((a0_len + a1_len) as Cost) * DIVMOD_COST_PER_BYTE;
-    check_cost(cost, max_cost)?;
-    if a1.sign() == malachite_bigint::Sign::NoSign {
-        return Err(EvalErr::DivisionByZero(input));
-    }
-    let (q, r) = a0.div_mod_floor(&a1);
-    let q1 = a.new_malachite_number(q)?;
-    let r1 = a.new_malachite_number(r)?;
-
-    let c = (a.atom_len(q1) + a.atom_len(r1)) as Cost * MALLOC_COST_PER_BYTE;
-    let r: NodePtr = a.new_pair(q1, r1)?;
-    Ok(Reduction(cost + c, r))
+    op_divmod_impl_inner(
+        a,
+        input,
+        max_cost,
+        limit,
+        malachite_int_atom,
+        |v: &malachite_bigint::BigInt| v.sign() == malachite_bigint::Sign::NoSign,
+        Allocator::new_malachite_number,
+    )
 }
 
 pub fn op_divmod(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response {
@@ -710,18 +742,41 @@
 }
 
 fn op_mod_impl(a: &mut Allocator, input: NodePtr, max_cost: Cost, limit: bool) -> Response {
+    op_mod_impl_inner(
+        a,
+        input,
+        max_cost,
+        limit,
+        int_atom,
+        |v: &Number| v.sign() == Sign::NoSign,
+        Allocator::new_number,
+    )
+}
+
+fn op_mod_impl_inner<T>(
+    a: &mut Allocator,
+    input: NodePtr,
+    max_cost: Cost,
+    limit: bool,
+    int_atom_fn: fn(&Allocator, NodePtr, &str) -> crate::error::Result<(T, usize)>,
+    is_zero: fn(&T) -> bool,
+    new_number_fn: fn(&mut Allocator, T) -> crate::error::Result<NodePtr>,
+) -> Response
+where
+    T: Integer,
+{
     let [v0, v1] = get_args::<2>(a, input, "mod")?;
-    let (a0, a0_len) = int_atom(a, v0, "mod")?;
-    let (a1, a1_len) = int_atom(a, v1, "mod")?;
+    let (a0, a0_len) = int_atom_fn(a, v0, "mod")?;
+    let (a1, a1_len) = int_atom_fn(a, v1, "mod")?;
     if limit && a0_len > 2048 {
         return Err(EvalErr::InvalidOpArg(input, "mod".to_string()));
     }
     let cost = DIV_BASE_COST + ((a0_len + a1_len) as Cost) * DIV_COST_PER_BYTE;
     check_cost(cost, max_cost)?;
-    if a1.sign() == Sign::NoSign {
+    if is_zero(&a1) {
         return Err(EvalErr::DivisionByZero(input));
     }
-    let q = a.new_number(a0.mod_floor(&a1))?;
+    let q = new_number_fn(a, a0.mod_floor(&a1))?;
     let c = a.atom_len(q) as Cost * MALLOC_COST_PER_BYTE;
     Ok(Reduction(cost + c, q))
 }
@@ -732,20 +787,15 @@
     max_cost: Cost,
     limit: bool,
 ) -> Response {
-    let [v0, v1] = get_args::<2>(a, input, "mod")?;
-    let (a0, a0_len) = malachite_int_atom(a, v0, "mod")?;
-    let (a1, a1_len) = malachite_int_atom(a, v1, "mod")?;
-    if limit && a0_len > 2048 {
-        return Err(EvalErr::InvalidOpArg(input, "mod".to_string()));
-    }
-    let cost = DIV_BASE_COST + ((a0_len + a1_len) as Cost) * DIV_COST_PER_BYTE;
-    check_cost(cost, max_cost)?;
-    if a1.sign() == malachite_bigint::Sign::NoSign {
-        return Err(EvalErr::DivisionByZero(input));
-    }
-    let q = a.new_malachite_number(a0.mod_floor(&a1))?;
-    let c = a.atom_len(q) as Cost * MALLOC_COST_PER_BYTE;
-    Ok(Reduction(cost + c, q))
+    op_mod_impl_inner(
+        a,
+        input,
+        max_cost,
+        limit,
+        malachite_int_atom,
+        |v: &malachite_bigint::BigInt| v.sign() == malachite_bigint::Sign::NoSign,
+        Allocator::new_malachite_number,
+    )
 }
 
 pub fn op_mod(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response {
@@ -1151,61 +1201,67 @@
 }
 
 pub fn op_modpow(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response {
+    op_modpow_impl(
+        a,
+        input,
+        max_cost,
+        int_atom,
+        |v: &Number| v.sign() == Sign::Minus,
+        |v: &Number| v.sign() == Sign::NoSign,
+        |base, exponent, modulus| base.modpow(exponent, modulus),
+        Allocator::new_number,
+    )
+}
+
+fn op_modpow_impl<T>(
+    a: &mut Allocator,
+    input: NodePtr,
+    max_cost: Cost,
+    int_atom_fn: fn(&Allocator, NodePtr, &str) -> crate::error::Result<(T, usize)>,
+    is_negative: fn(&T) -> bool,
+    is_zero: fn(&T) -> bool,
+    modpow_fn: fn(&T, &T, &T) -> T,
+    new_number_fn: fn(&mut Allocator, T) -> crate::error::Result<NodePtr>,
+) -> Response {
     let [base, exponent, modulus] = get_args::<3>(a, input, "modpow")?;
 
     let mut cost = MODPOW_BASE_COST;
-    let (base, bsize) = int_atom(a, base, "modpow")?;
+    let (base, bsize) = int_atom_fn(a, base, "modpow")?;
     cost += bsize as Cost * MODPOW_COST_PER_BYTE_BASE_VALUE;
-    let (exponent, esize) = int_atom(a, exponent, "modpow")?;
+    let (exponent, esize) = int_atom_fn(a, exponent, "modpow")?;
     cost += (esize * esize) as Cost * MODPOW_COST_PER_BYTE_EXPONENT;
     check_cost(cost, max_cost)?;
-    let (modulus, msize) = int_atom(a, modulus, "modpow")?;
+    let (modulus, msize) = int_atom_fn(a, modulus, "modpow")?;
     cost += (msize * msize) as Cost * MODPOW_COST_PER_BYTE_MOD;
     check_cost(cost, max_cost)?;
 
-    if exponent.sign() == Sign::Minus {
+    if is_negative(&exponent) {
         return Err(EvalErr::InvalidOpArg(
             input,
             "ModPow with Negative Exponent".to_string(),
         ));
     }
 
-    if modulus.sign() == Sign::NoSign {
+    if is_zero(&modulus) {
         return Err(EvalErr::DivisionByZero(input));
     }
 
-    let ret = base.modpow(&exponent, &modulus);
-    let ret = a.new_number(ret)?;
+    let ret = modpow_fn(&base, &exponent, &modulus);
+    let ret = new_number_fn(a, ret)?;
     Ok(malloc_cost(a, cost, ret))
 }
 
 pub fn op_modpow_malachite(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response {
-    let [base, exponent, modulus] = get_args::<3>(a, input, "modpow")?;
-
-    let mut cost = MODPOW_BASE_COST;
-    let (base, bsize) = malachite_int_atom(a, base, "modpow")?;
-    cost += bsize as Cost * MODPOW_COST_PER_BYTE_BASE_VALUE;
-    let (exponent, esize) = malachite_int_atom(a, exponent, "modpow")?;
-    cost += (esize * esize) as Cost * MODPOW_COST_PER_BYTE_EXPONENT;
-    check_cost(cost, max_cost)?;
-    let (modulus, msize) = malachite_int_atom(a, modulus, "modpow")?;
-    cost += (msize * msize) as Cost * MODPOW_COST_PER_BYTE_MOD;
-    check_cost(cost, max_cost)?;
-
-    if exponent.sign() == malachite_bigint::Sign::Minus {
-        return Err(EvalErr::InvalidOpArg(
-            input,
-            "ModPow with Negative Exponent".to_string(),
-        ));
-    }
-
-    if modulus.sign() == malachite_bigint::Sign::NoSign {
-        return Err(EvalErr::DivisionByZero(input));
-    }
-
-    let ret = base.modpow(&exponent, &modulus);
-    let ret = a.new_malachite_number(ret)?;
-    Ok(malloc_cost(a, cost, ret))
+    op_modpow_impl(
+        a,
+        input,
+        max_cost,
+        malachite_int_atom,
+        |v: &malachite_bigint::BigInt| v.sign() == malachite_bigint::Sign::Minus,
+        |v: &malachite_bigint::BigInt| v.sign() == malachite_bigint::Sign::NoSign,
+        |base, exponent, modulus| base.modpow(exponent, modulus),
+        Allocator::new_malachite_number,
+    )
 }
 
 #[cfg(test)]
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@socket-security
Copy link

socket-security bot commented Mar 9, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​malachite-bigint@​0.9.11001009310070

View full report

@socket-security
Copy link

socket-security bot commented Mar 9, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • cargo/bytemuck@1.25.0
  • cargo/foldhash@0.2.0
  • cargo/libm@0.2.16
  • cargo/malachite-base@0.9.1
  • cargo/malachite-bigint@0.9.1
  • cargo/malachite-nz@0.9.1
  • cargo/paste@1.0.15
  • cargo/safe_arch@1.0.0
  • cargo/wide@1.1.1
  • cargo/itertools@0.14.0
  • cargo/ryu@1.0.23

View full report

@coveralls-official
Copy link

coveralls-official bot commented Mar 9, 2026

Pull Request Test Coverage Report for Build 22865941586

Details

  • 161 of 196 (82.14%) changed or added relevant lines in 6 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage decreased (-0.08%) to 88.131%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/allocator.rs 38 40 95.0%
src/chia_dialect.rs 11 26 42.31%
src/more_ops.rs 92 110 83.64%
Files with Coverage Reduction New Missed Lines %
src/chia_dialect.rs 1 70.7%
Totals Coverage Status
Change from base Build 22864957331: -0.08%
Covered Lines: 7136
Relevant Lines: 8097

💛 - Coveralls

@arvidn arvidn force-pushed the malachite-arithmetic branch from 5a135a5 to d9ea087 Compare March 9, 2026 09:27
@arvidn arvidn changed the title use malachite as a bignum dependency where it has more efficient implementations malachite Mar 9, 2026
@arvidn arvidn requested review from Rigidity and wjblanke March 9, 2026 11:31
@arvidn
Copy link
Contributor Author

arvidn commented Mar 9, 2026

@SocketSecurity ignore-all

Copy link
Contributor

@wjblanke wjblanke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aok

@arvidn arvidn enabled auto-merge March 9, 2026 18:36
@arvidn arvidn merged commit ea732dc into main Mar 9, 2026
32 checks passed
@arvidn arvidn deleted the malachite-arithmetic branch March 9, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants