From 7d9af348db3f118f6c5136630fe89481fac8325f Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:19:39 -0700 Subject: [PATCH 1/3] feat: make ndarray std support conditional ndarray is now optional with default-features = false. A new std feature is part of default and forwards ndarray?/std, so a normal build that also enables ndarray-bindings still compiles ndarray with std, while std alone does not activate ndarray. CI checks the resolved graph: no std token for --no-default-features --features ndarray-bindings, std present for std,ndarray-bindings and default,ndarray-bindings, and ndarray absent for std alone and for the other feature rows. Tests cover both ndarray-bindings configurations, and the changelog notes that disabling std only controls ndarray's standard-library support and does not yet make the rest of smartcore usable without std. Fixes #421 --- .github/workflows/ci.yml | 94 +++++++++++++++++++++++++++++++++++++++- CHANGELOG.md | 1 + Cargo.toml | 16 +++++-- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7715aed7..db4a0270 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,7 @@ jobs: - "--features serde" - "--features datasets" - "--features ndarray-bindings" + - "--features std,ndarray-bindings" - "" steps: - uses: actions/checkout@v4 @@ -72,9 +73,100 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Stable Build run: cargo build --no-default-features ${{ matrix.features }} + - name: Assert ndarray feature resolution + shell: bash + run: | + set -euo pipefail + + # `cargo tree --edges normal,build` omits dev-dependency edges. + # `{p} {f}` is "name version feat1,feat2". Match the ndarray package + # name exactly and compare the comma-separated feature tokens to + # "std" so a substring on another package cannot pass. + assert_ndarray() { + local expect="$1" + shift + local tree line features has_std token found + # Assign separately so `local` cannot mask a failing `cargo tree`. + tree="$(cargo tree --edges normal,build --prefix none --format '{p} {f}' "$@")" + found=0 + while IFS= read -r line; do + [ -z "$line" ] && continue + # Cargo appends " (*)" on repeated packages; it is not a feature. + line="${line% (*)}" + case "$line" in + ndarray[[:space:]]*) ;; + *) continue ;; + esac + found=1 + # Drop the package name and version; the rest is `{f}`. + features="$(printf '%s\n' "$line" | awk '{ $1=""; $2=""; sub(/^[[:space:]]+/, ""); print }')" + has_std=0 + if [ -n "$features" ]; then + IFS=',' read -ra tokens <<< "$features" + for token in "${tokens[@]}"; do + token="${token#"${token%%[![:space:]]*}"}" + token="${token%"${token##*[![:space:]]}"}" + if [ "$token" = "std" ]; then + has_std=1 + fi + done + fi + if [ "$expect" = "absent" ]; then + echo "ndarray unexpectedly present in the normal/build graph: $line" >&2 + exit 1 + fi + if [ "$expect" = "std" ] && [ "$has_std" -ne 1 ]; then + echo "ndarray is missing the std feature token: $line" >&2 + exit 1 + fi + if [ "$expect" = "no-std" ] && [ "$has_std" -ne 0 ]; then + echo "ndarray unexpectedly enables std: $line" >&2 + exit 1 + fi + echo "ndarray feature check passed ($expect): $line" + done <<< "$tree" + if [ "$found" -eq 0 ]; then + if [ "$expect" = "absent" ]; then + echo "ndarray is absent from the normal/build graph" + return 0 + fi + echo "expected ndarray package line is missing" >&2 + printf '%s\n' "$tree" >&2 + exit 1 + fi + } + + case "${{ matrix.features }}" in + "--features ndarray-bindings") + assert_ndarray no-std --no-default-features ${{ matrix.features }} + ;; + "--features std,ndarray-bindings") + assert_ndarray std --no-default-features ${{ matrix.features }} + ;; + "") + assert_ndarray absent --no-default-features + # Default features and explicit std must not activate ndarray. + # This catches strong `ndarray/std` forwarding. + assert_ndarray absent + assert_ndarray absent --no-default-features --features std + # Default plus bindings keeps ndarray's std feature on. + assert_ndarray std --features default,ndarray-bindings + ;; + *) + assert_ndarray absent --no-default-features ${{ matrix.features }} + ;; + esac - name: Tests - if: matrix.features == '--features ndarray-bindings' + if: matrix.features == '--features ndarray-bindings' || matrix.features == '--features std,ndarray-bindings' run: cargo test --no-default-features ${{ matrix.features }} + - name: Explicit std does not activate ndarray + if: matrix.features == '' + run: cargo build --no-default-features --features std + - name: Default plus ndarray-bindings + if: matrix.features == '' + run: | + cargo build --features default,ndarray-bindings + cargo test --features default,ndarray-bindings msrv: # Verify the declared rust-version (MSRV) in Cargo.toml still builds. diff --git a/CHANGELOG.md b/CHANGELOG.md index c250a5db..7e330a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `predict`, `predict_proba`, `predict_oob`, `predict_matrix` and `decision_function` now return `Err(Failed)` instead of panicking when `fit` has not run (#469, #470). Models return the same unfitted error even after deserialization, when the state fields are `None`. The guarded methods cover the decision trees, the random forests, the extra-trees regressor, the KNN classifier and regressor, `SVC`, `MultiClassSVC`, `SVR`, the linear models, the naive Bayes classifiers and `XGRegressor`. ### Changed +- `Cargo.toml`: optional `ndarray` is now `default-features = false`, and the default feature set is `std` (`ndarray?/std`). Ordinary builds still enable ndarray's standard-library support when `ndarray-bindings` is on, while `std` alone does not activate ndarray. Disabling `std` controls ndarray's std support at this stage; it does not yet make all of smartcore usable on a bare-metal target (#421). - The error text for an unfitted `MultiClassSVC::predict` changed from "MultiClassSVC is not fitted" to the common "'fit' should be called before calling 'predict'". Code that matched the old string must be updated. ## [0.6.15] diff --git a/Cargo.toml b/Cargo.toml index 4ec3a4b5..ffa01965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ exclude = [ [dependencies] approx = "0.5.1" cfg-if = "1.0.0" -ndarray = { version = "0.17", optional = true } +ndarray = { version = "0.17", optional = true, default-features = false } num-traits = "0.2.12" num = "0.4" rand = { version = "0.10.1", default-features = false, features = ["alloc"] } @@ -35,9 +35,17 @@ ordered-float = "5.1.0" typetag = { version = "0.2", optional = true } [features] -# No features enabled by default; keeps the build WASM-compatible -# and avoids pulling in serde/rand unless explicitly requested. -default = [] +# `std` stays on by default so ordinary builds keep ndarray's standard-library +# support when `ndarray-bindings` is enabled. Weak forwarding (`ndarray?/std`) +# does not activate the optional ndarray dependency by itself. Disabling `std` +# controls ndarray's std support at this stage; it does not yet make all of +# smartcore usable on a bare-metal target. serde and rand stay opt-in, which +# keeps the default dependency set WASM-compatible. +default = ["std"] + +# Enable ndarray's `std` feature when ndarray is already enabled. +# The `?` keeps `--features std` from pulling ndarray in on its own. +std = ["ndarray?/std"] # Enable serde Serialize/Deserialize for models and related types. # Also enables typetag on non-wasm targets for trait object serialization. From 6848000815ba7ca3364e71f4aac89a448a132762 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:26:59 -0700 Subject: [PATCH 2/3] ci: check ndarray features with a script and note the migration Move the ndarray feature assertion out of ci.yml into ci/check-ndarray-features.sh, which reads the resolved feature list from `cargo tree -i ndarray --depth 0` instead of parsing the whole tree. Add a CHANGELOG migration note for `--no-default-features --features ndarray-bindings` builds, and drop the bare-metal wording: the `std` feature only controls ndarray's std support. --- .github/workflows/ci.yml | 75 ++++-------------------------------- CHANGELOG.md | 3 +- Cargo.toml | 8 ++-- ci/check-ndarray-features.sh | 30 +++++++++++++++ 4 files changed, 43 insertions(+), 73 deletions(-) create mode 100755 ci/check-ndarray-features.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db4a0270..71bbc182 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,84 +76,23 @@ jobs: - name: Assert ndarray feature resolution shell: bash run: | - set -euo pipefail - - # `cargo tree --edges normal,build` omits dev-dependency edges. - # `{p} {f}` is "name version feat1,feat2". Match the ndarray package - # name exactly and compare the comma-separated feature tokens to - # "std" so a substring on another package cannot pass. - assert_ndarray() { - local expect="$1" - shift - local tree line features has_std token found - # Assign separately so `local` cannot mask a failing `cargo tree`. - tree="$(cargo tree --edges normal,build --prefix none --format '{p} {f}' "$@")" - found=0 - while IFS= read -r line; do - [ -z "$line" ] && continue - # Cargo appends " (*)" on repeated packages; it is not a feature. - line="${line% (*)}" - case "$line" in - ndarray[[:space:]]*) ;; - *) continue ;; - esac - found=1 - # Drop the package name and version; the rest is `{f}`. - features="$(printf '%s\n' "$line" | awk '{ $1=""; $2=""; sub(/^[[:space:]]+/, ""); print }')" - has_std=0 - if [ -n "$features" ]; then - IFS=',' read -ra tokens <<< "$features" - for token in "${tokens[@]}"; do - token="${token#"${token%%[![:space:]]*}"}" - token="${token%"${token##*[![:space:]]}"}" - if [ "$token" = "std" ]; then - has_std=1 - fi - done - fi - if [ "$expect" = "absent" ]; then - echo "ndarray unexpectedly present in the normal/build graph: $line" >&2 - exit 1 - fi - if [ "$expect" = "std" ] && [ "$has_std" -ne 1 ]; then - echo "ndarray is missing the std feature token: $line" >&2 - exit 1 - fi - if [ "$expect" = "no-std" ] && [ "$has_std" -ne 0 ]; then - echo "ndarray unexpectedly enables std: $line" >&2 - exit 1 - fi - echo "ndarray feature check passed ($expect): $line" - done <<< "$tree" - if [ "$found" -eq 0 ]; then - if [ "$expect" = "absent" ]; then - echo "ndarray is absent from the normal/build graph" - return 0 - fi - echo "expected ndarray package line is missing" >&2 - printf '%s\n' "$tree" >&2 - exit 1 - fi - } - case "${{ matrix.features }}" in "--features ndarray-bindings") - assert_ndarray no-std --no-default-features ${{ matrix.features }} + ci/check-ndarray-features.sh no-std --no-default-features ${{ matrix.features }} ;; "--features std,ndarray-bindings") - assert_ndarray std --no-default-features ${{ matrix.features }} + ci/check-ndarray-features.sh std --no-default-features ${{ matrix.features }} ;; "") - assert_ndarray absent --no-default-features + ci/check-ndarray-features.sh absent --no-default-features # Default features and explicit std must not activate ndarray. - # This catches strong `ndarray/std` forwarding. - assert_ndarray absent - assert_ndarray absent --no-default-features --features std + ci/check-ndarray-features.sh absent + ci/check-ndarray-features.sh absent --no-default-features --features std # Default plus bindings keeps ndarray's std feature on. - assert_ndarray std --features default,ndarray-bindings + ci/check-ndarray-features.sh std --features default,ndarray-bindings ;; *) - assert_ndarray absent --no-default-features ${{ matrix.features }} + ci/check-ndarray-features.sh absent --no-default-features ${{ matrix.features }} ;; esac - name: Tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e330a35..4334b8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `predict`, `predict_proba`, `predict_oob`, `predict_matrix` and `decision_function` now return `Err(Failed)` instead of panicking when `fit` has not run (#469, #470). Models return the same unfitted error even after deserialization, when the state fields are `None`. The guarded methods cover the decision trees, the random forests, the extra-trees regressor, the KNN classifier and regressor, `SVC`, `MultiClassSVC`, `SVR`, the linear models, the naive Bayes classifiers and `XGRegressor`. ### Changed -- `Cargo.toml`: optional `ndarray` is now `default-features = false`, and the default feature set is `std` (`ndarray?/std`). Ordinary builds still enable ndarray's standard-library support when `ndarray-bindings` is on, while `std` alone does not activate ndarray. Disabling `std` controls ndarray's std support at this stage; it does not yet make all of smartcore usable on a bare-metal target (#421). +- `Cargo.toml`: optional `ndarray` is now `default-features = false`, and the default feature set is `std` (`ndarray?/std`). Ordinary builds still enable ndarray's standard-library support when `ndarray-bindings` is on, while `std` alone does not activate ndarray. The `std` feature only controls ndarray's standard-library support; smartcore itself is not `no_std` (#421). + - **Migration:** builds that use `--no-default-features --features ndarray-bindings` (or `default-features = false` with `ndarray-bindings`) no longer get ndarray's `std` feature. Add `std` to the feature list to keep the previous behavior. - The error text for an unfitted `MultiClassSVC::predict` changed from "MultiClassSVC is not fitted" to the common "'fit' should be called before calling 'predict'". Code that matched the old string must be updated. ## [0.6.15] diff --git a/Cargo.toml b/Cargo.toml index ffa01965..b94a1a0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,10 +37,10 @@ typetag = { version = "0.2", optional = true } [features] # `std` stays on by default so ordinary builds keep ndarray's standard-library # support when `ndarray-bindings` is enabled. Weak forwarding (`ndarray?/std`) -# does not activate the optional ndarray dependency by itself. Disabling `std` -# controls ndarray's std support at this stage; it does not yet make all of -# smartcore usable on a bare-metal target. serde and rand stay opt-in, which -# keeps the default dependency set WASM-compatible. +# does not activate the optional ndarray dependency by itself. `std` only +# controls ndarray's standard-library support; smartcore itself is not +# `no_std`. serde and rand stay opt-in, which keeps the default dependency set +# WASM-compatible. default = ["std"] # Enable ndarray's `std` feature when ndarray is already enabled. diff --git a/ci/check-ndarray-features.sh b/ci/check-ndarray-features.sh new file mode 100755 index 00000000..0737f671 --- /dev/null +++ b/ci/check-ndarray-features.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Assert how ndarray resolves for a set of cargo feature flags. +# Usage: ci/check-ndarray-features.sh [cargo feature flags...] +set -euo pipefail + +expect="$1" +shift + +# `-i ndarray --depth 0` prints only the ndarray node, as "ndarray vX.Y.Z|feat1,feat2". +# Dev-dependency edges are excluded so test-only crates cannot affect the result. +if ! line="$(cargo tree -q -e normal,build -i ndarray --depth 0 --format '{p}|{f}' "$@" 2>&1)"; then + case "$line" in + *"did not match any packages"*) line="" ;; + *) printf '%s\n' "$line" >&2; exit 1 ;; + esac +fi + +if [ -z "$line" ]; then + actual="absent" +elif [[ ",${line#*|}," == *",std,"* ]]; then + actual="std" +else + actual="no-std" +fi + +if [ "$actual" != "$expect" ]; then + echo "ndarray with [$*]: expected $expect, got $actual (${line:-not in graph})" >&2 + exit 1 +fi +echo "ndarray with [$*]: $actual" From f3a41960133b5ecd977ec7eabfa795bc60dd565f Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:02:00 -0700 Subject: [PATCH 3/3] docs: list the ndarray std change under 0.7.0 The change is breaking for builds that use default-features = false with ndarray-bindings, so it belongs with the next minor release rather than the 0.6.16 entries. --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4334b8ff..7d0dfa47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] +### Changed +- `Cargo.toml`: optional `ndarray` is now `default-features = false`, and the default feature set is `std` (`ndarray?/std`). Ordinary builds still enable ndarray's standard-library support when `ndarray-bindings` is on, while `std` alone does not activate ndarray. The `std` feature only controls ndarray's standard-library support; smartcore itself is not `no_std` (#421). + - **Migration:** builds that use `--no-default-features --features ndarray-bindings` (or `default-features = false` with `ndarray-bindings`) no longer get ndarray's `std` feature. Add `std` to the feature list to keep the previous behavior. + ## [0.6.16] ### Fixed - `predict`, `predict_proba`, `predict_oob`, `predict_matrix` and `decision_function` now return `Err(Failed)` instead of panicking when `fit` has not run (#469, #470). Models return the same unfitted error even after deserialization, when the state fields are `None`. The guarded methods cover the decision trees, the random forests, the extra-trees regressor, the KNN classifier and regressor, `SVC`, `MultiClassSVC`, `SVR`, the linear models, the naive Bayes classifiers and `XGRegressor`. ### Changed -- `Cargo.toml`: optional `ndarray` is now `default-features = false`, and the default feature set is `std` (`ndarray?/std`). Ordinary builds still enable ndarray's standard-library support when `ndarray-bindings` is on, while `std` alone does not activate ndarray. The `std` feature only controls ndarray's standard-library support; smartcore itself is not `no_std` (#421). - - **Migration:** builds that use `--no-default-features --features ndarray-bindings` (or `default-features = false` with `ndarray-bindings`) no longer get ndarray's `std` feature. Add `std` to the feature list to keep the previous behavior. - The error text for an unfitted `MultiClassSVC::predict` changed from "MultiClassSVC is not fitted" to the common "'fit' should be called before calling 'predict'". Code that matched the old string must be updated. ## [0.6.15]