Skip to content

v2: add NonZeroF*, FiniteF*, NonSubnormalF* class-based float niche types - #3

Merged
xinyandai merged 2 commits into
mainfrom
v2-float-niche-classes
Jul 24, 2026
Merged

v2: add NonZeroF*, FiniteF*, NonSubnormalF* class-based float niche types#3
xinyandai merged 2 commits into
mainfrom
v2-float-niche-classes

Conversation

@xinyandai

Copy link
Copy Markdown
Owner

Closes #1.

Adds the three class-based float niche families deferred from v1, each for f32 and f64, all built on the crate's existing (anchor bit-pattern, validity predicate) model — one niche_float_class! invocation apiece plus an Ord-capability choice.

New types

Type Rejects Anchor Ord/Eq/Hash
NonZeroF32 / NonZeroF64 zero as a class (+0.0 and -0.0) +0.0 bits no (can hold NaN)
FiniteF32 / FiniteF64 non-finite (NaN and ±inf) a NaN pattern yes (never NaN)
NonSubnormalF32 / NonSubnormalF64 subnormals a subnormal pattern no (can hold NaN)
  • NonZeroF* rejecting zero as a class is deliberately distinct from the bit-exact NonValueF32<0x0000_0000>, which forbids only +0.0 and leaves -0.0 valid — that footgun is exactly why the value-class type earns its own name.
  • FiniteF* = NonNan* ∩ NonInf*; because it can never hold NaN, it gets a total Ord/Eq/Hash (same -0.0-normalized Hash as NonNan*).

One design note (small deviation from the issue's "no new machinery")

The issue assumed each type is just a macro invocation. The existing niche_float_class! took reject = <method-ident> and called value.$method(), but NonZero (v == 0.0) and Finite (!v.is_finite()) have no single-method predicate. So the macro's reject clause is generalized from a bare method ident to a predicate closure; the existing NonNan*/NonInf* invocations are updated to the closure form with unchanged behavior (covered by the existing tests). No other machinery changed.

Acceptance criteria

  • Six types via niche_float_class! with the specified anchors
  • FiniteF* use impl_total_ord!; the other four use impl_partial_ord!
  • Per-type tests: class rejection, round-trip, and Option<T> size assertion
  • Miri clean; README type table + crate docs updated

Verification (all clean, run locally)

  • cargo test --all-features — 28 tests + doctest pass
  • cargo build --no-default-features (no_std) — builds
  • cargo clippy --all-features --all-targets -- -D warnings — clean
  • RUSTDOCFLAGS="-D warnings" cargo doc --all-features — no broken links
  • cargo +nightly miri test --all-features — clean (28 tests + doctest)

Out of scope

Fatter niches that reclaim an entire forbidden class as multi-variant enum room — those need unstable rustc_layout_scalar_valid_range and violate the crate's stable-only constraint.

🤖 Generated with Claude Code

xinyandai and others added 2 commits July 24, 2026 02:14
…e types

Implements issue #1. Adds six class-based float niche types (each for f32
and f64) via the existing (anchor, validity-predicate) model:

- NonZeroF32/64  — reject zero as a *class* (+0.0 and -0.0); anchor +0.0 bits.
  Distinct from bit-exact NonValueF32<0x0000_0000>, which forbids only +0.0.
- FiniteF32/64   — reject non-finite (NaN and ±inf); anchor a NaN pattern.
  Cannot hold NaN, so gets total Ord/Eq/Hash (= NonNan ∩ NonInf).
- NonSubnormalF32/64 — reject subnormals; anchor a subnormal pattern.

FiniteF* use impl_total_ord!; NonZeroF*/NonSubnormalF* use impl_partial_ord!
(they can still hold NaN).

The niche_float_class! macro's `reject` clause is generalized from a bare
method ident (`value.$m()`) to a predicate closure, since NonZero (`v == 0.0`)
and Finite (`!v.is_finite()`) have no single-method form. Existing NonNan/NonInf
invocations are updated to the closure form; behavior is unchanged.

Per-type tests cover class rejection, round-trip, and Option<T> niche size.
Verified: cargo test --all-features, no_std build, clippy -D warnings,
rustdoc -D warnings, and cargo +nightly miri test --all-features (all clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- int: gate `u16 -> NonMaxUsize` / `i16 -> NonMaxIsize` off 16-bit targets,
  where the source is not strictly narrower than usize/isize and would widen to
  the forbidden MAX, passing 0 to NonZero::new_unchecked (UB) through a safe
  From. Add a const size-ordering assert in widen_prim! as a compile-time
  backstop against future same-width pairs.
- docs: correct the "always by bit pattern" claim. Bit-exact types reject by
  pattern; class-based types classify by value but anchor on a rejected-class
  pattern, sound iff the anchor == itself (+0.0 for NonZero, is_nan() for
  NonNan). Update float/lib module docs and README.
- float serde: document that bit identity (NaN payload, signed zero) survives
  only on IEEE-bit-preserving formats; add a signed-zero / NaN-payload test.
- tests: exercise `primitive & niche` bitand with distinct operands; add
  negative-subnormal rejection; dedupe the NonNan/Finite total-order+hash
  contract into shared helpers; gate std-only tests behind cfg(feature = "std")
  and run `cargo test --no-default-features` in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xinyandai
xinyandai merged commit 8cb5267 into main Jul 24, 2026
6 checks passed
@xinyandai
xinyandai deleted the v2-float-niche-classes branch July 24, 2026 18:39
@xinyandai xinyandai mentioned this pull request Jul 24, 2026
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.

v2: additional class-based float niche types (NonZeroF*, FiniteF*, NonSubnormalF*)

1 participant