From aa635248e13cb34cd62b906fc9b0c9ce765f9248 Mon Sep 17 00:00:00 2001 From: axect Date: Mon, 6 Jul 2026 21:04:57 +0800 Subject: [PATCH] FEAT: Make rand / rand_distr optional behind default-on rand feature (#88) Sandboxed targets (Typst plugins, wasm32) cannot use stateful RNG, so the random sampling stack is now optional: * rand / rand_distr become optional dependencies behind a new default-on `rand` feature; existing users see no change, and `default-features = false` drops the RNG stack entirely. * Gated behind the feature: statistics::dist, statistics::rand, util::wrapper, the rand() / rand_with_rng() / rand_with_dist() constructors, the runif! / rnorm! / dnorm! / pnorm! / rt! / dt! / pt! / rand! macros, the dist Printable impls, and the fuga / prelude re-exports of all of the above. * complex/matrix.rs imported num_traits through rand_distr's re-export; it now uses num-traits directly (new optional dep tied to the complex feature). * Examples that sample (dist, matmul, optim, clippy_verify) declare required-features = ["rand"]; rand-dependent tests and doctests are cfg-gated. * New CI job: cargo test --no-default-features plus a wasm32-unknown-unknown build, so the deterministic core (ODE, integration, splines, linear algebra) cannot silently regress. Verified locally: default 472 tests, complex 512 tests, no-default 444 tests, wasm32 build, clippy zero errors in both configurations. --- .github/workflows/Github.yml | 14 ++++++++++++++ Cargo.toml | 32 ++++++++++++++++++++++++++++---- README.md | 1 + src/complex/matrix.rs | 2 +- src/fuga/mod.rs | 12 ++++++++++-- src/lib.rs | 1 + src/macros/matlab_macro.rs | 1 + src/macros/r_macro.rs | 7 +++++++ src/numerical/optimize.rs | 2 ++ src/prelude/mod.rs | 13 +++++++++++-- src/statistics/mod.rs | 6 ++++-- src/statistics/stat.rs | 2 ++ src/structure/matrix.rs | 2 ++ src/util/mod.rs | 1 + src/util/non_macro.rs | 6 ++++++ src/util/print.rs | 8 +++++++- tests/dist.rs | 2 ++ tests/matrix.rs | 1 + tests/weighted_uniform.rs | 2 ++ 19 files changed, 103 insertions(+), 12 deletions(-) diff --git a/.github/workflows/Github.yml b/.github/workflows/Github.yml index 638636cb..dd231f1e 100644 --- a/.github/workflows/Github.yml +++ b/.github/workflows/Github.yml @@ -45,6 +45,20 @@ jobs: - name: Run tests run: cargo test --verbose --features "arrow complex csv indexmap json num-complex parallel parquet rayon rkyv serde" + # The deterministic core (no `rand` stack) must keep building for sandboxed + # targets such as Typst / wasm plugins (#88). + no-rand-wasm: + name: No-rand core (wasm32) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Add wasm32 target + run: rustup target add wasm32-unknown-unknown + - name: Test without the rand stack + run: cargo test --verbose --no-default-features + - name: Build for wasm32 + run: cargo build --verbose --no-default-features --target wasm32-unknown-unknown + feature-combinations: name: Feature combinations (cargo-hack) runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index da0ad834..c96ff8c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ criterion = { version = "0.5.1", features = ["html_reports"] } [dependencies] csv = { version = "1.3", optional = true, default-features = false } -rand = { version = "0.9", features = ["small_rng"] } -rand_distr = "0.5" +rand = { version = "0.9", features = ["small_rng"], optional = true } +rand_distr = { version = "0.5", optional = true } order-stat = "0.1" puruspe = "0.4" matrixmultiply = { version = "0.3", features = ["threading"] } @@ -54,6 +54,7 @@ parquet = { version = "55", features = ["arrow", "snap"], optional = true } arrow = { version = "55", optional = true } indexmap = { version = "1", optional = true } num-complex = { version = "0.4", optional = true } +num-traits = { version = "0.2", optional = true } rayon = { version = "1.10", optional = true } [package.metadata.docs.rs] @@ -75,8 +76,31 @@ features = [ "serde", ] +# Examples that exercise the random sampling stack are skipped when the +# `rand` feature is disabled. +[[example]] +name = "dist" +required-features = ["rand"] + +[[example]] +name = "matmul" +required-features = ["rand"] + +[[example]] +name = "optim" +required-features = ["rand"] + +[[example]] +name = "clippy_verify" +required-features = ["rand"] + [features] -default = [] +default = ["rand"] +# Random sampling stack (`rand` / `rand_distr`): probability distributions, +# RNG wrappers, and random constructors. Enabled by default; disable with +# `default-features = false` for the deterministic core (ODE, integration, +# splines, linear algebra), e.g. for wasm32 sandboxes without IO. +rand = ["dep:rand", "dep:rand_distr"] # Bare BLAS / LAPACK FFI; downstream binary must also pull in a # `blas-src` / `lapack-src` backend to resolve the link symbols. O3 = ["blas", "lapack"] @@ -88,5 +112,5 @@ O3-netlib = ["O3", "blas-src/netlib", "lapack-src/netlib"] plot = ["pyo3"] nc = ["netcdf"] parquet = ["dep:parquet", "arrow", "indexmap"] -complex = ["num-complex", "matrixmultiply/cgemm"] +complex = ["num-complex", "num-traits", "matrixmultiply/cgemm"] parallel = ["rayon"] diff --git a/README.md b/README.md index 1bb249d9..84faceb6 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,7 @@ The remaining single-crate flags exist so advanced users can pull in just one op | `json` | (pure Rust) | JSON I/O for `DataFrame` | | `serde` | (pure Rust) | `serde` (de)serialization | | `rkyv` | (pure Rust) | `rkyv` zero-copy (de)serialization | +| `rand` | (pure Rust) | Random sampling stack: distributions, RNG wrappers, `rand()` constructors. **On by default**; disable with `default-features = false` to get the deterministic core (ODE, integration, splines, linear algebra) for sandboxed targets like wasm32 |
Advanced: single-crate flags diff --git a/src/complex/matrix.rs b/src/complex/matrix.rs index 6162fcad..f126843a 100644 --- a/src/complex/matrix.rs +++ b/src/complex/matrix.rs @@ -7,8 +7,8 @@ use std::{ use anyhow::{bail, Result}; use matrixmultiply::CGemmOption; use num_complex::Complex; +use num_traits::{One, Zero}; use peroxide_num::{ExpLogOps, PowOps, TrigOps}; -use rand_distr::num_traits::{One, Zero}; use crate::{ complex::C64, diff --git a/src/fuga/mod.rs b/src/fuga/mod.rs index 892647f5..321e8c63 100644 --- a/src/fuga/mod.rs +++ b/src/fuga/mod.rs @@ -183,10 +183,17 @@ pub use crate::complex::{integral::*, matrix::*, vector::*, C64}; #[allow(ambiguous_glob_reexports)] pub use crate::structure::{ad::*, dataframe::*, matrix::*, polynomial::*, vector::*}; -pub use crate::util::{api::*, low_level::*, non_macro::*, print::*, useful::*, wrapper::*}; +pub use crate::util::{api::*, low_level::*, non_macro::*, print::*, useful::*}; +#[cfg(feature = "rand")] +pub use crate::util::wrapper::*; + +#[allow(unused_imports)] +pub use crate::statistics::{ops::*, stat::*}; + +#[cfg(feature = "rand")] #[allow(unused_imports)] -pub use crate::statistics::{dist::*, ops::*, rand::*, stat::*}; +pub use crate::statistics::{dist::*, rand::*}; #[allow(unused_imports)] pub use crate::special::function::*; @@ -205,6 +212,7 @@ pub use crate::util::plot::*; pub use anyhow; pub use paste; +#[cfg(feature = "rand")] pub use rand::prelude::*; // ============================================================================= diff --git a/src/lib.rs b/src/lib.rs index cab7066d..26985205 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,6 +172,7 @@ extern crate pyo3; #[cfg(feature = "serde")] extern crate serde; +#[cfg(feature = "rand")] extern crate rand; // extern crate json; diff --git a/src/macros/matlab_macro.rs b/src/macros/matlab_macro.rs index c643cc88..c9c2c5e7 100644 --- a/src/macros/matlab_macro.rs +++ b/src/macros/matlab_macro.rs @@ -39,6 +39,7 @@ macro_rules! zeros { /// println!("{}", a); // 2 x 2 random matrix (0 ~ 1) /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! rand { () => {{ diff --git a/src/macros/r_macro.rs b/src/macros/r_macro.rs index 6742e3e8..28bf8c1c 100644 --- a/src/macros/r_macro.rs +++ b/src/macros/r_macro.rs @@ -269,6 +269,7 @@ macro_rules! rbind { /// println!("{:?}", b); /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! runif { ( $x0:expr, $start:expr, $end:expr ) => {{ @@ -304,6 +305,7 @@ macro_rules! runif { /// println!("{:?}", b); /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! rnorm { ( $n:expr, $mean:expr, $sd:expr ) => {{ @@ -333,6 +335,7 @@ macro_rules! rnorm { /// println!("{:?}", b); /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! dnorm { ( $x:expr, $mean: expr, $sd:expr ) => {{ @@ -364,6 +367,7 @@ macro_rules! dnorm { /// println!("{:?}", b); /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! pnorm { ( $x:expr, $mean:expr, $sd:expr ) => {{ @@ -392,6 +396,7 @@ macro_rules! pnorm { /// println!("{:?}", a); /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! rt { ( $n:expr, $df:expr ) => {{ @@ -414,6 +419,7 @@ macro_rules! rt { /// println!("{:?}", a); /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! dt { ( $x:expr, $df:expr ) => {{ @@ -437,6 +443,7 @@ macro_rules! dt { /// println!("{:?}", a); // 0.5 /// } /// ``` +#[cfg(feature = "rand")] #[macro_export] macro_rules! pt { ( $x:expr, $df:expr ) => {{ diff --git a/src/numerical/optimize.rs b/src/numerical/optimize.rs index b4eac36d..2e94cccb 100644 --- a/src/numerical/optimize.rs +++ b/src/numerical/optimize.rs @@ -53,6 +53,7 @@ //! use peroxide::fuga::*; //! //! fn main() { +//! # #[cfg(feature = "rand")] { //! // To prepare noise //! let normal = Normal(0f64, 0.1f64); //! let normal2 = Normal(0f64, 100f64); @@ -94,6 +95,7 @@ //! // .set_marker(vec![Point, Line]) //! // .savefig().expect("Can't draw a plot"); //! //} +//! # } //! } //! //! fn quad(x: &Vec, n: Vec) -> Option> { diff --git a/src/prelude/mod.rs b/src/prelude/mod.rs index 6c46b349..823af049 100644 --- a/src/prelude/mod.rs +++ b/src/prelude/mod.rs @@ -191,10 +191,18 @@ pub use crate::complex::{integral::*, matrix::*, vector::*, C64}; pub use simpler::{solve, SimplerLinearAlgebra}; #[allow(unused_imports)] -pub use crate::util::{api::*, low_level::*, non_macro::*, print::*, useful::*, wrapper::*}; +pub use crate::util::{api::*, low_level::*, non_macro::*, print::*, useful::*}; +#[cfg(feature = "rand")] #[allow(unused_imports)] -pub use crate::statistics::{dist::*, ops::*, rand::*, stat::*}; +pub use crate::util::wrapper::*; + +#[allow(unused_imports)] +pub use crate::statistics::{ops::*, stat::*}; + +#[cfg(feature = "rand")] +#[allow(unused_imports)] +pub use crate::statistics::{dist::*, rand::*}; #[allow(unused_imports)] pub use crate::special::function::{ @@ -228,4 +236,5 @@ pub use crate::util::plot::*; pub use anyhow; pub use paste; +#[cfg(feature = "rand")] pub use rand::prelude::*; diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 3012a917..c128b652 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -1,11 +1,13 @@ //! Statistical Modules //! //! * Basic statistical tools - `stat.rs` -//! * Popular distributions - `dist.rs` -//! * Simple Random Number Generator - `rand.rs` +//! * Popular distributions - `dist.rs` (`rand` feature) +//! * Simple Random Number Generator - `rand.rs` (`rand` feature) //! * Basic probabilistic operations - `ops.rs` +#[cfg(feature = "rand")] pub mod dist; pub mod ops; +#[cfg(feature = "rand")] pub mod rand; pub mod stat; diff --git a/src/statistics/stat.rs b/src/statistics/stat.rs index d2160b20..738b73a0 100644 --- a/src/statistics/stat.rs +++ b/src/statistics/stat.rs @@ -552,6 +552,7 @@ pub fn cor(v1: &Vec, v2: &Vec) -> f64 { /// use peroxide::fuga::*; /// /// fn main() { +/// # #[cfg(feature = "rand")] { /// let a: Matrix = c!(1,2,3,4,5).into(); /// let noise: Matrix = Normal(0,1).sample(5).into(); /// let b: Matrix = &a + &noise; @@ -560,6 +561,7 @@ pub fn cor(v1: &Vec, v2: &Vec) -> f64 { /// // c[0] /// // r[0] 0.7219 /// // r[1] 0.8058 +/// # } /// } /// ``` pub fn lm(input: &Matrix, target: &Matrix) -> Matrix { diff --git a/src/structure/matrix.rs b/src/structure/matrix.rs index 963d6c3d..1e45b6da 100644 --- a/src/structure/matrix.rs +++ b/src/structure/matrix.rs @@ -351,8 +351,10 @@ //! let b = eye(2); //! assert_eq!(b, ml_matrix("1 0;0 1")); //! +//! # #[cfg(feature = "rand")] { //! let c = rand(2, 2); //! c.print(); // Random 2x2 matrix +//! # } //! } //! ``` //! # Linear Algebra diff --git a/src/util/mod.rs b/src/util/mod.rs index a5bffe0b..b39f2963 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -9,5 +9,6 @@ pub mod plot; pub mod low_level; pub mod print; pub mod useful; +#[cfg(feature = "rand")] pub mod wrapper; pub mod writer; diff --git a/src/util/non_macro.rs b/src/util/non_macro.rs index edfd4a2f..9a49eda7 100644 --- a/src/util/non_macro.rs +++ b/src/util/non_macro.rs @@ -30,7 +30,9 @@ //! - concat //! - cat +#[cfg(feature = "rand")] extern crate rand; +#[cfg(feature = "rand")] use self::rand::prelude::*; use crate::structure::{ matrix::Shape::{Col, Row}, @@ -39,6 +41,7 @@ use crate::structure::{ use crate::traits::float::FloatWithPrecision; use crate::traits::matrix::MatrixTrait; use anyhow::{bail, Result}; +#[cfg(feature = "rand")] use rand_distr::{Distribution, Uniform}; #[derive(Debug, Copy, Clone)] @@ -322,6 +325,7 @@ where /// # Description /// /// Range = from 0 to 1 +#[cfg(feature = "rand")] pub fn rand(r: usize, c: usize) -> Matrix { let mut rng = rand::rng(); rand_with_rng(r, c, &mut rng) @@ -332,6 +336,7 @@ pub fn rand(r: usize, c: usize) -> Matrix { /// # Description /// /// Range = from 0 to 1 +#[cfg(feature = "rand")] pub fn rand_with_rng(r: usize, c: usize, rng: &mut R) -> Matrix { let uniform = Uniform::new_inclusive(0f64, 1f64).unwrap(); rand_with_dist(r, c, rng, uniform) @@ -342,6 +347,7 @@ pub fn rand_with_rng(r: usize, c: usize, rng: &mut R) -> Matrix { /// # Description /// /// Any range +#[cfg(feature = "rand")] pub fn rand_with_dist, R: Rng, D: Distribution>( r: usize, c: usize, diff --git a/src/util/print.rs b/src/util/print.rs index 5ff93bf8..3ba6b9cf 100644 --- a/src/util/print.rs +++ b/src/util/print.rs @@ -1,5 +1,6 @@ //! Easy to print any structures +#[cfg(feature = "rand")] use crate::statistics::dist::*; use crate::statistics::stat::ConfusionMatrix; #[allow(unused_imports)] @@ -10,8 +11,11 @@ use crate::structure::{ multinomial::Multinomial, polynomial::Polynomial, }; +#[cfg(feature = "rand")] use rand_distr::uniform::SampleUniform; -use std::fmt::{Debug, LowerExp, UpperExp}; +#[cfg(feature = "rand")] +use std::fmt::Debug; +use std::fmt::{LowerExp, UpperExp}; pub trait Printable { fn print(&self); @@ -355,12 +359,14 @@ impl Printable for Multinomial { // } //} +#[cfg(feature = "rand")] impl> Printable for OPDist { fn print(&self) { println!("{:?}", self); } } +#[cfg(feature = "rand")] impl> Printable for TPDist { fn print(&self) { println!("{:?}", self); diff --git a/tests/dist.rs b/tests/dist.rs index 883a1d39..14dcad17 100644 --- a/tests/dist.rs +++ b/tests/dist.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "rand")] + extern crate peroxide; use peroxide::fuga::*; diff --git a/tests/matrix.rs b/tests/matrix.rs index d36aed6d..6f0bb0c5 100644 --- a/tests/matrix.rs +++ b/tests/matrix.rs @@ -54,6 +54,7 @@ fn test_row() { assert_eq!(a.row(0), c!(1, 2)); } +#[cfg(feature = "rand")] #[test] fn test_print() { let op = Bernoulli(0); diff --git a/tests/weighted_uniform.rs b/tests/weighted_uniform.rs index e7204995..e3c36c2d 100644 --- a/tests/weighted_uniform.rs +++ b/tests/weighted_uniform.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "rand")] + extern crate peroxide; use peroxide::fuga::*;