Skip to content

fix(cfloat): frexp returns a [0.5,1) fraction (std::frexp semantics)#1029

Merged
Ravenwater merged 1 commit into
mainfrom
fix/issue-1027-cfloat-frexp-semantics
May 28, 2026
Merged

fix(cfloat): frexp returns a [0.5,1) fraction (std::frexp semantics)#1029
Ravenwater merged 1 commit into
mainfrom
fix/issue-1027-cfloat-frexp-semantics

Conversation

@Ravenwater
Copy link
Copy Markdown
Contributor

@Ravenwater Ravenwater commented May 28, 2026

Summary

cfloat's frexp returned a fraction in [1,2) with *exp = scale() (= floor(log2|x|)), which does not match std::frexp (fraction in [0.5,1), *exp = floor(log2|x|)+1) -- issue #1027. Generic code written to the std contract -- and every other Universal type's frexp (dd, qd, ereal, bfloat16 all use [0.5,1)) -- saw an off-by-one exponent. cfloat was the outlier.

The fix (cfloat_impl.hpp)

  • Place the fraction at scale -1 so |fraction| lands in [0.5,1); *exp = scale()+1.
  • ldexp unchanged -- it rebuilds the exponent from scale(), so ldexp(frexp(x,&e),e) == x holds for every input (the change setexponent(0)->(-1) is invisible to the round-trip).
  • Added the std special cases: +-0/inf/nan return unchanged with *exp = 0 (the old code ran setexponent on them and corrupted them).
  • Low-range fallback: es <= 2 configs (minimum normal exponent >= 0) cannot represent any value below 1.0 as a normal, so [0.5,1) is unachievable; a compile-time check keeps the [1,2) fraction there. Round-trip still holds.

Dependency sweep (why this is safe -- the requested analysis)

Swept every frexp call site across include/, static/, elastic/, applications/, tools/:

  • Only callers of cfloat's frexp are round-trip tests (fractional.cpp) -- convention-agnostic (they only assert ldexp(frexp(x),e)==x).
  • No generic/templated code instantiates cfloat's frexp expecting [1,2); the dd/qd/ereal/cascade callers use their own frexp (already [0.5,1)).
  • cfloat manipulators/attributes/conversions/numeric_limits do not use frexp.
  • No call site depends on the [1,2) convention. Blast radius: frexp + its test (+ one doc comment). No ldexp change, no other types.

Changes

  • include/sw/universal/number/cfloat/cfloat_impl.hpp -- frexp std semantics + special cases + es<=2 fallback
  • static/float/cfloat/math/fractional.cpp -- assert [0.5,1) fraction range for normals (where representable), frexp(0)/inf/nan cases, + half/cfloat<16,8> coverage
  • elastic/elreal/arithmetic/exact_value_oracle.cpp -- comment updated (cfloat frexp/floor now fixed; oracle keeps its self-contained bit-based extraction)

Test Results

Target gcc clang
cfloat_fractional (frexp range + 0/inf/nan + fmod) PASS PASS
el_arith_exact_value_oracle (#1022, regression) PASS PASS

Note (separate, not fixed here)

cfloat's isnormal() reports true for ±0 (std::isnormal(0) is false). It didn't affect frexp (which uses iszero()), but I had to exclude zero from the test's range assertion explicitly. Happy to file it if you'd like.

Test plan

  • Fast CI passes
  • Promote to ready when satisfied

Resolves #1027

Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed frexp behavior to correctly match standard library semantics when handling special values such as ±0, infinity, and NaN.
  • Documentation

    • Clarified internal documentation for bit-level implementation details.
  • Tests

    • Expanded test coverage with additional configurations to validate fraction and exponent handling across wider scenarios.

Review Change Stack

cfloat's frexp returned a fraction in [1,2) with *exp = scale() (floor(log2|x|)),
which does not match std::frexp (fraction in [0.5,1), *exp = floor(log2|x|)+1) --
issue #1027. Generic code written to the std contract (and every other Universal
type's frexp: dd, qd, ereal, bfloat16 all use [0.5,1)) saw the wrong exponent.

Now: place the fraction at scale -1 (*exp = scale()+1). Extreme low-range configs
(es <= 2, minimum normal exponent >= 0) cannot represent any value below 1.0 as a
normal, so [0.5,1) is unachievable there; those fall back to the [1,2) fraction.
ldexp is UNCHANGED -- it rebuilds the exponent from scale(), so the round-trip
ldexp(frexp(x,&e),e) == x holds in every case. Also added the std special cases:
+-0/inf/nan return unchanged with *exp = 0 (the old code corrupted them).

Dependency sweep (the reason this is safe): the only callers of cfloat's frexp
are round-trip tests, which are convention-agnostic; no generic/templated code
instantiates cfloat's frexp expecting [1,2); manipulators/attributes/conversions
do not use it. So the change is confined to frexp + its test.

Test (fractional.cpp): assert the [0.5,1) fraction range for normal inputs where
representable, plus frexp(0)/inf/nan special cases; added half and cfloat<16,8>
coverage. (Note: a separate cfloat quirk -- isnormal() reports true for +-0 --
required excluding zero from the range assertion explicitly.)

Verified gcc + clang: cfloat_fractional passes; the #1022 elreal oracle (which
reads cfloat bits directly) is regression-clean.

Resolves #1027

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

This PR fixes cfloat's frexp function to return mantissas in the [0.5, 1) range as specified by std::frexp, instead of the previous [1, 2) behavior. The implementation adds proper handling for special cases (±0, inf, NaN), updates verification tests to assert the new range, extends regression coverage, and updates related documentation.

Changes

frexp Implementation and Verification

Layer / File(s) Summary
frexp semantics fix
include/sw/universal/number/cfloat/cfloat_impl.hpp
frexp is rewritten to compute a configuration-dependent targetScale, handle ±0/inf/NaN by returning unchanged values with *exp=0, and adjust the returned fraction's exponent so normal values fall in [0.5,1) where representable, matching std::frexp contract.
frexp verification and regression tests
static/float/cfloat/math/fractional.cpp
VerifyCfloatFractionExponent adds representable-range assertions (
exact oracle documentation update
elastic/elreal/arithmetic/exact_value_oracle.cpp
Documentation comment for exact_real()'s bit-layout extraction path is reworded to emphasize self-contained bit-reading and cross-width consistency verification, reflecting the oracle's implementation approach.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • stillwater-sc/universal#995: Introduces the exact-value oracle program in elreal whose documentation is updated to reflect the bit-layout extraction approach that circumvents the prior frexp semantics issue.

Suggested labels

bug

🐰 A mantissa's rightful place,
Once strayed in [one, two)'s space!
Now frexp bounds align so true,
[Half, one) where standard says it's due—
Off-by-one exponent bid adieu! ✓

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: fixing cfloat's frexp to match std::frexp semantics with [0.5,1) fraction range.
Linked Issues check ✅ Passed The pull request fully implements the requirements from issue #1027: frexp now returns mantissa in [0.5,1), *exp = x.scale()+1, handles special cases (±0/inf/nan), includes fallback for es<=2, and adds tests verifying the range and round-trip equality.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #1027: frexp implementation fix in cfloat_impl.hpp, verification updates in fractional.cpp test, and a related documentation comment in exact_value_oracle.cpp with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1027-cfloat-frexp-semantics

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@static/float/cfloat/math/fractional.cpp`:
- Around line 51-58: The frexp special-case tests for TestType only check the
returned value classification but leave the exponent variable e initialized to
0, so a failure to set exp isn't detected; change the local exponent
initialization in both frexp(inf, &e) and frexp(nan, &e) blocks to a non-zero
sentinel (e.g., -1), then after calling frexp assert that e == 0 as well as
f.isinf()/f.isnan(), and on failure increment nrOfFailedTests and print a clear
message (use the existing reportTestCases flag and messages like "frexp(inf)
FAIL: exp != 0" / "frexp(nan) FAIL: exp != 0") so both classification and
exponent reset are validated for TestType.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6099b684-88d4-4d40-9514-b8c98d7fa482

📥 Commits

Reviewing files that changed from the base of the PR and between c04efce and 403817d.

📒 Files selected for processing (3)
  • elastic/elreal/arithmetic/exact_value_oracle.cpp
  • include/sw/universal/number/cfloat/cfloat_impl.hpp
  • static/float/cfloat/math/fractional.cpp

Comment on lines +51 to +58
if (std::numeric_limits<TestType>::has_infinity) {
TestType inf; inf.setinf(false); int e = 0; TestType f = frexp(inf, &e);
if (!f.isinf()) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(inf) FAIL\n"; }
}
if (std::numeric_limits<TestType>::has_quiet_NaN) {
TestType nan; nan.setnan(); int e = 0; TestType f = frexp(nan, &e);
if (!f.isnan()) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(nan) FAIL\n"; }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate exp == 0 for inf/nan special cases, not just classification.

Line 52 and Line 56 initialize e to 0, so these checks cannot detect failure to reset exp. Seed with a non-zero sentinel and assert e == 0 after frexp.

Proposed test tightening
 if (std::numeric_limits<TestType>::has_infinity) {
-    TestType inf; inf.setinf(false); int e = 0; TestType f = frexp(inf, &e);
-    if (!f.isinf()) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(inf) FAIL\n"; }
+    TestType inf; inf.setinf(false); int e = -99; TestType f = frexp(inf, &e);
+    if (!f.isinf() || e != 0) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(inf) FAIL: e=" << e << '\n'; }
 }
 if (std::numeric_limits<TestType>::has_quiet_NaN) {
-    TestType nan; nan.setnan(); int e = 0; TestType f = frexp(nan, &e);
-    if (!f.isnan()) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(nan) FAIL\n"; }
+    TestType nan; nan.setnan(); int e = -99; TestType f = frexp(nan, &e);
+    if (!f.isnan() || e != 0) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(nan) FAIL: e=" << e << '\n'; }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (std::numeric_limits<TestType>::has_infinity) {
TestType inf; inf.setinf(false); int e = 0; TestType f = frexp(inf, &e);
if (!f.isinf()) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(inf) FAIL\n"; }
}
if (std::numeric_limits<TestType>::has_quiet_NaN) {
TestType nan; nan.setnan(); int e = 0; TestType f = frexp(nan, &e);
if (!f.isnan()) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(nan) FAIL\n"; }
}
if (std::numeric_limits<TestType>::has_infinity) {
TestType inf; inf.setinf(false); int e = -99; TestType f = frexp(inf, &e);
if (!f.isinf() || e != 0) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(inf) FAIL: e=" << e << '\n'; }
}
if (std::numeric_limits<TestType>::has_quiet_NaN) {
TestType nan; nan.setnan(); int e = -99; TestType f = frexp(nan, &e);
if (!f.isnan() || e != 0) { ++nrOfFailedTests; if (reportTestCases) std::cout << "frexp(nan) FAIL: e=" << e << '\n'; }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/float/cfloat/math/fractional.cpp` around lines 51 - 58, The frexp
special-case tests for TestType only check the returned value classification but
leave the exponent variable e initialized to 0, so a failure to set exp isn't
detected; change the local exponent initialization in both frexp(inf, &e) and
frexp(nan, &e) blocks to a non-zero sentinel (e.g., -1), then after calling
frexp assert that e == 0 as well as f.isinf()/f.isnan(), and on failure
increment nrOfFailedTests and print a clear message (use the existing
reportTestCases flag and messages like "frexp(inf) FAIL: exp != 0" / "frexp(nan)
FAIL: exp != 0") so both classification and exponent reset are validated for
TestType.

@Ravenwater Ravenwater marked this pull request as ready for review May 28, 2026 16:49
@Ravenwater Ravenwater self-assigned this May 28, 2026
@Ravenwater Ravenwater added the bug label May 28, 2026
@Ravenwater Ravenwater added this to the V4 milestone May 28, 2026
@Ravenwater Ravenwater merged commit d8a88d7 into main May 28, 2026
32 checks passed
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 26588939352

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.01%) to 84.017%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: 5 of 5 lines across 1 file are fully covered (100%).
  • 3 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

3 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
include/sw/universal/number/posito/posito_impl.hpp 2 89.78%
include/sw/universal/number/posit1/specialized/posit_16_1.hpp 1 80.8%

Coverage Stats

Coverage Status
Relevant Lines: 55859
Covered Lines: 46931
Line Coverage: 84.02%
Coverage Strength: 5782595.63 hits per line

💛 - Coveralls

@Ravenwater Ravenwater deleted the fix/issue-1027-cfloat-frexp-semantics branch May 28, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

cfloat: frexp returns mantissa in [1,2), not std::frexp's [0.5,1)

2 participants