Skip to content

feat(elreal): Phase B -- rational and string construction with lazy refinement#884

Merged
Ravenwater merged 4 commits into
mainfrom
feat/elreal-phase-b
May 21, 2026
Merged

feat(elreal): Phase B -- rational and string construction with lazy refinement#884
Ravenwater merged 4 commits into
mainfrom
feat/elreal-phase-b

Conversation

@Ravenwater

@Ravenwater Ravenwater commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase B of epic #873. Implements #875.

Activates the lazy refinement protocol that Phase A reserved a slot for, and adds the construction-from-source machinery: rational elreal(p, q) and string elreal("1/3") / elreal("3.14") / elreal("6.022e23").

The acceptance criterion of #875 -- elreal("1/3").at(1) != 0, observably distinct from elreal(1.0/3.0) -- is met because the rational path computes the exact residual p - c0 * q via the existing EFTs (two_prod + two_sum).

What changed

  • Generator slot promoted from reserved-comment in Phase A to a real mutable std::function<double(std::size_t)> member. at(k) calls it on cache miss; refine_to(precision_bits) iterates at(0..ceil(p/53)).
  • Rational constructor elreal(long long, long long):
    • Component 0: double(p) / double(q)
    • Component 1: exact residual via EFTs, exact for |p| < 2^53
    • Components 2+: 0.0 with a documented Phase B limitation. Arbitrary-depth McCleeary long division is deferred to Phase E/F.
    • q == 0 -> NaN; p == 0 -> canonical zero.
  • String constructor elreal(const std::string&):
    • Decimal: "3.14", "-2.5"
    • Scientific: "6.022e23", "-1.0e-10"
    • Rational: "1/3", "-22/7"
    • Special tokens (case-insensitive): "nan", "inf", "infinity", with optional sign
    • Parser normalises decimal / scientific to (p, q) form and delegates to the rational ctor. When the normalised numerator or denominator exceeds long long range (typical for 6.022e23), falls back to double reconstruction -- leading component faithful, exact-rational refinement unavailable.
    • Silent-failure ctor pattern matching dfloat and ereal; parse(str, elreal&) is the bool-returning variant for callers that want to observe errors.
  • operator double() fix: starts the sum at _components[0] instead of 0.0 so single-component -0.0 round-trips with its sign bit preserved (otherwise 0.0 + (-0.0) == +0.0 swallows the sign).

Cross-construction from dd / qd / ereal

Marked optional in #875 and deferred in this PR. The include-graph cost of pulling in those types' headers from inside elreal_impl.hpp is non-trivial; a follow-up PR can land them targeted, once their value is clearer.

Test plan

Four test binaries under elastic/elreal/:

Binary What it covers
elreal_api Phase A canary (unchanged)
elreal_native_types round-trip for int/float/double/subnormal/inf/nan/-0.0
elreal_rational elreal(1,3).at(1) != 0; observably distinct from elreal(1.0/3.0); 1/2 and 1/4 exact in one component; -3/4 exact; 1/0 -> NaN; 0/7 -> zero
elreal_string decimals, scientific, rationals, special tokens, parse-failure rejection
  • gcc 13 (build_elreal/): all four PASS
  • clang (build_clang_elreal/): all four PASS

Canary excerpt:

elreal(1, 3):
  at(0)            = 0.333333
  at(1)            = 1.85037e-17
  at(0) + at(1)    = 0.333333
  double(1.0/3.0)  = 0.333333
elreal Phase B rational construction: PASS

at(1) = 1.85e-17 -- the exact correction 1/3 - double(1/3), which is what the acceptance criterion of #875 asks for.

What does not land in this PR

Related

Summary by CodeRabbit

  • New Features

    • Rational constructor with improved residual refinement and on-demand multi-component generation.
    • String parsing for decimals, scientific notation, rationals, NaN and ±infinity, with sensible fallbacks and signed-zero preservation.
    • Operator/behavior updates to preserve negative zero and to materialize components lazily.
  • Tests

    • New test suites covering native-type round-trips, rational construction, string parsing, edge cases (NaN, infinities, ±0), overflow/underflow, and parse-failure handling.

Review Change Stack

…inement

Phase B of epic #873 (resolves #875): adds the construction-from-source
machinery and activates the lazy refinement protocol for rational inputs.

Generator slot promoted from reserved-comment to real member:
- mutable std::function<double(std::size_t)> _generator
- at(k) walks the generator on cache miss, appending to _components
- refine_to(precision_bits) iterates at(0..ceil(p/53))
- An absent generator (e.g. for elreal(double) values) leaves at(k>=depth)
  returning the implicit-zero extension, same as Phase A

Rational constructor elreal(long long p, long long q):
- Component 0: double(p) / double(q) (IEEE-754 rounded)
- Component 1 (via generator): exact residual p - c0 * q computed with
  two_prod + two_sum, divided by q. Exact for |p| < 2^53 (i.e. any
  long long whose absolute value is within the 53-bit double-significand
  envelope); incurs one extra rounding for larger magnitudes.
- Components 2+: 0.0 with a documented limitation. Arbitrary-depth
  refinement via McCleeary long division is deferred to Phase E/F.
- elreal(p, 0) -> NaN; elreal(0, q) -> canonical zero.

String constructor elreal(const std::string&):
- Accepts decimal ("3.14"), scientific ("6.022e23"), and rational ("1/3")
  forms, plus the special tokens nan / inf / infinity (case-insensitive,
  with optional sign).
- The parser normalises decimal / scientific to (p, q) form and delegates
  to the rational constructor. The acceptance criterion of #875 --
  elreal("1/3").at(1) != 0 -- is met because the rational path returns
  a non-trivial correction.
- Overflow fallback: scientific-notation values whose normalised numerator
  or denominator exceeds long long range fall back to a double-precision
  reconstruction (mantissa * pow(10, exp)). The leading component is
  faithful; exact-rational refinement past component 0 is unavailable.
  This handles e.g. "6.022e23" gracefully.
- Silent-failure pattern matching dfloat / ereal: the std::string ctor
  swallows parse errors and returns canonical zero. parse(str, elreal&)
  is the bool-returning variant for callers that want to observe errors.

operator double() fix: start the sum at _components[0] instead of 0.0 so
that single-component -0.0 round-trips with its IEEE sign bit preserved.

Tests under elastic/elreal/conversion/:
- native_types.cpp: round-trip int/float/double/subnormal/inf/nan/-0.0
- rational.cpp: elreal(1, 3) delivers a non-zero correction; refinement
  is observably distinct from elreal(1.0/3.0); 1/2 and 1/4 are exact in
  a single component; -3/4 is exact; 1/0 -> NaN; 0/7 -> zero
- string.cpp: decimal, scientific, rational, special-token, and parse-
  failure cases

Cross-construction from dd / qd / ereal is left as future work; #875
marks it optional and the include-graph cost is non-trivial.

Verified clean under gcc 13 and clang on dual-build-dir setup.

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

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81e41eb1-789f-4a88-93b2-d2130ee14ee3

📥 Commits

Reviewing files that changed from the base of the PR and between 3016552 and 901ec08.

📒 Files selected for processing (2)
  • elastic/elreal/conversion/string.cpp
  • include/sw/universal/number/elreal/elreal_impl.hpp

📝 Walkthrough

Walkthrough

Implements ELREAL Phase B: adds rational and string constructors, a lazy generator for residual components, updates component materialization and double conversion to preserve IEEE semantics, and adds test programs validating native-type round-trips, rational refinement, and string parsing.

Changes

ELREAL Phase B: Constructors, Parsing, and Lazy Refinement

Layer / File(s) Summary
Header documentation and Phase B includes
include/sw/universal/number/elreal/elreal_impl.hpp
Phase A/B documentation is expanded and headers are updated to include functional/generator and EFT helpers required for Phase B residual computation.
Rational and string constructors with generator setup
include/sw/universal/number/elreal/elreal_impl.hpp
elreal(long long p, long long q) sets component0 and installs a generator closure to compute the exact residual via EFT; explicit elreal(const std::string&) delegates to parse_into. Both handle NaN for q==0 and canonical zero for p==0.
Lazy component materialization and refinement
include/sw/universal/number/elreal/elreal_impl.hpp
at(k) now invokes _generator on cache miss to materialize components and append to _components; refine_to(precision_bits) forces materialization of a computed prefix.
operator double() with -0.0 preservation
include/sw/universal/number/elreal/elreal_impl.hpp
operator double() sums starting from _components[0] and fast-paths to 0.0 when _computed_depth==0 to preserve IEEE -0.0 behavior.
Parser implementation and generator member
include/sw/universal/number/elreal/elreal_impl.hpp
Adds _generator member and implements parse_into(elreal&, const std::string&) to parse whitespace, sign, nan/inf, rational p/q (with negative-zero preservation), decimal/scientific forms, integer-range normalization, and overflow fallback; updates parse(str,out) to call parse_into.
Native type round-trip validation
elastic/elreal/conversion/native_types.cpp
Test program validating round-trip conversion for native integers and doubles through elreal, covering subnormal/special values, NaN, infinities, and signed -0.0 preservation; failures reported via test helpers.
Rational construction and refinement validation
elastic/elreal/conversion/rational.cpp
Test program verifying exactness for simple rationals, refinement component behavior for 1/3 (at(0)/at(1) properties and error reduction), distinction between elreal(1,3) and elreal(1.0/3.0), and special-case NaN/zero handling.
String parsing and format validation
elastic/elreal/conversion/string.cpp
Test program validating parsing of decimal/scientific/rational formats, special tokens (nan, inf), mantissa/overflow fallback behavior vs std::stod, malformed-input parse outcomes, and canonical-zero for invalid inputs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • stillwater-sc/universal#883: Phase B work in this PR extends the Phase A elreal skeleton introduced in #883 by adding generator-based at/refine_to behavior and new constructors/parsing.
  • stillwater-sc/universal#838: Related through string-literal parsing primitives that support parse_into-based implementations.

Poem

"I’m a rabbit with a careful hop and pen,
I parse the strings and mend the fraction's den,
I wake the generator to coax each bit,
Keep zero’s sign tidy—neat and fit,
Hop—Phase B lands softly in my glen."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% 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 'feat(elreal): Phase B -- rational and string construction with lazy refinement' accurately and specifically summarizes the main changes: implementation of Phase B with rational/string construction and lazy refinement protocol.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/elreal-phase-b

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

@Ravenwater Ravenwater self-assigned this May 20, 2026
@Ravenwater Ravenwater added this to the V4 milestone May 20, 2026
@Ravenwater Ravenwater moved this to In progress in Universal Number Library May 20, 2026
@Ravenwater Ravenwater marked this pull request as ready for review May 20, 2026 23:30

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
elastic/elreal/conversion/string.cpp (2)

71-94: ⚡ Quick win

Cover optional sign and surrounding whitespace for special-token parsing.

Line 71 tests case-insensitivity well, but parser behavior also claims optional sign and whitespace handling. Add token cases like " +inf " (and optionally " -inf ") to prevent regressions in that path.

🤖 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 `@elastic/elreal/conversion/string.cpp` around lines 71 - 94, Add test inputs
that cover optional leading/trailing whitespace and optional sign for
special-token parsing: extend the nan and inf blocks that construct elreal from
std::string (symbols: elreal, isnan(), isinf(), double(elreal)) to also create
and assert parsing of strings like "  +inf  ", "  -inf  " (and similarly "  NaN 
" / "  +NaN  " if desired); ensure the new +inf/inf variants call isinf() and
that negative variants produce negative infinity by checking double(...) < 0.0,
and treat whitespace-wrapped tokens as valid parsed values just like the
existing cases.

34-52: ⚡ Quick win

Add an explicit negative-zero parsing assertion.

Line 35 onwards validates numeric equality, but -0.0 == 0.0 so this does not verify sign-bit preservation for string input. Add a dedicated "-0" case using std::signbit(double(...)) to lock the -0.0 contract.

Proposed test addition
@@
 `#include` <universal/number/elreal/elreal.hpp>
 `#include` <universal/verification/test_suite.hpp>
+#include <cmath>
@@
 	// --- decimal integers ------------------------------------------------
 	nrOfFailedTestCases += check_string_value("0",          0.0);
@@
 	nrOfFailedTestCases += check_string_value("+12",        12.0);
+	{
+		elreal neg_zero{ std::string("-0") };
+		if (!std::signbit(double(neg_zero))) {
+			std::cerr << "FAIL: elreal(\"-0\") did not preserve negative zero\n";
+			++nrOfFailedTestCases;
+		}
+	}
🤖 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 `@elastic/elreal/conversion/string.cpp` around lines 34 - 52, Add a dedicated
test for the "-0" input to verify negative-zero preservation: after parsing the
string "-0" using the same parser used by check_string_value (locate the parsing
call inside or adjacent to check_string_value), assert that
std::signbit(parsed_double) is true (rather than relying on == comparison). Keep
the existing numeric-equality tests and add this extra signbit assertion so the
"-0.0" sign-bit contract is locked in.
elastic/elreal/conversion/native_types.cpp (1)

62-69: 💤 Low value

Optional: prefer !std::signbit(...) over != true.

The logic is correct, but line 65 could be slightly more idiomatic:

if (!std::signbit(double(x))) {

instead of

if (std::signbit(double(x)) != true) {

Both work; the former is the more common C++ style.

🤖 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 `@elastic/elreal/conversion/native_types.cpp` around lines 62 - 69, Replace the
explicit comparison to true in the signbit check with the idiomatic negation: in
the block that defines nz and elreal x (symbols: nz, x, elreal) change the
condition using std::signbit(double(x)) != true to use !std::signbit(double(x));
leave the rest of the failure handling (nrOfFailedTestCases increment and error
message) unchanged.
🤖 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 `@include/sw/universal/number/elreal/elreal_impl.hpp`:
- Around line 561-567: The code negates p when computing pmag (long long pmag =
p < 0 ? -p : p), which is UB if p == LLONG_MIN; change pmag to an unsigned type
and compute its magnitude without signed negation — e.g. use unsigned long long
pmag and set pmag = p < 0 ? static_cast<unsigned long long>(-(p + 1)) + 1ULL :
static_cast<unsigned long long>(p); then compare pmag against
std::numeric_limits<unsigned long long>::max() / 10 and update overflowed and p
(p *= 10) accordingly; update the loop in the q_pow < 0 branch (symbols: p,
pmag, overflowed, q_pow) so no signed overflow can occur.
- Around line 476-496: Parsing of the rational numerator/denominator (variables
num and den in the parsing block that checks slash != std::string::npos) does
not detect overflow when doing num = num * 10 + digit and den = den * 10 +
digit; modify both accumulation loops to check for overflow before
multiplying/adding (e.g., compare against std::numeric_limits<long long>::max():
if num > (LLONG_MAX - digit) / 10 then return false, and similarly for den) so
that the function returns false on overflow instead of silently wrapping; keep
the rest of the logic (found_num/found_den, den != 0, applying negative flag to
out) unchanged.
- Around line 498-522: The loop that accumulates mantissa (variable mantissa)
must detect overflow like the rational parser does: add a bool overflowed flag
and before doing mantissa = mantissa * 10 + (c - '0') check (e.g. compare
against LLONG_MAX/10 and digit) and set overflowed when it would wrap; continue
scanning digits but do not let undefined wrap occur. After the digit loop and
before handling the exponent ('e'/'E') branch, check overflowed and handle it
the same way as the rational parsing code path (e.g. reject/adjust parse or
branch to the overflow handling logic) so overflow is not silently ignored.
Ensure references to found_digit, seen_dot, frac_digits and pos remain
consistent.

---

Nitpick comments:
In `@elastic/elreal/conversion/native_types.cpp`:
- Around line 62-69: Replace the explicit comparison to true in the signbit
check with the idiomatic negation: in the block that defines nz and elreal x
(symbols: nz, x, elreal) change the condition using std::signbit(double(x)) !=
true to use !std::signbit(double(x)); leave the rest of the failure handling
(nrOfFailedTestCases increment and error message) unchanged.

In `@elastic/elreal/conversion/string.cpp`:
- Around line 71-94: Add test inputs that cover optional leading/trailing
whitespace and optional sign for special-token parsing: extend the nan and inf
blocks that construct elreal from std::string (symbols: elreal, isnan(),
isinf(), double(elreal)) to also create and assert parsing of strings like " 
+inf  ", "  -inf  " (and similarly "  NaN  " / "  +NaN  " if desired); ensure
the new +inf/inf variants call isinf() and that negative variants produce
negative infinity by checking double(...) < 0.0, and treat whitespace-wrapped
tokens as valid parsed values just like the existing cases.
- Around line 34-52: Add a dedicated test for the "-0" input to verify
negative-zero preservation: after parsing the string "-0" using the same parser
used by check_string_value (locate the parsing call inside or adjacent to
check_string_value), assert that std::signbit(parsed_double) is true (rather
than relying on == comparison). Keep the existing numeric-equality tests and add
this extra signbit assertion so the "-0.0" sign-bit contract is locked in.
🪄 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: ac8b4613-8979-4623-968f-2591168dc2c6

📥 Commits

Reviewing files that changed from the base of the PR and between 6ccc9f9 and 5d822c7.

📒 Files selected for processing (4)
  • elastic/elreal/conversion/native_types.cpp
  • elastic/elreal/conversion/rational.cpp
  • elastic/elreal/conversion/string.cpp
  • include/sw/universal/number/elreal/elreal_impl.hpp

Comment thread include/sw/universal/number/elreal/elreal_impl.hpp
Comment thread include/sw/universal/number/elreal/elreal_impl.hpp
Comment thread include/sw/universal/number/elreal/elreal_impl.hpp
@coveralls

coveralls commented May 20, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 26201081295

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.249%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 2 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
include/sw/universal/number/posito/posito_impl.hpp 2 89.78%

Coverage Stats

Coverage Status
Relevant Lines: 55729
Covered Lines: 46951
Line Coverage: 84.25%
Coverage Strength: 5276037.43 hits per line

💛 - Coveralls

Address CodeRabbit findings on PR #884.

Three correctness fixes in elreal_impl.hpp:

1. Rational parser num / den accumulation now rejects on signed-overflow
   (num * 10 + digit > LLONG_MAX) before the multiply happens. Without
   the guard, 19+ digit inputs silently wrapped.

2. Decimal / scientific parser mantissa accumulation gets the same guard.

3. The q_pow < 0 multiplication loop replaces the `pmag = (p < 0 ? -p :
   p)` magnitude with unsigned arithmetic so LLONG_MIN does not trigger
   signed-negation UB. With the mantissa overflow guard above, p cannot
   actually reach LLONG_MIN in this path, but the unsigned form is
   defensive against a future caller of the rational ctor with a
   hand-rolled value.

One consistency fix: parse("-0"), parse("-0.0"), parse("-0/7") now all
preserve the negative sign through to the resulting elreal. Previously
the parser collapsed these to canonical (sign-less) zero, inconsistent
with elreal(-0.0) which preserves the IEEE-754 sign bit. Sign now
survives across all "construct from -0" paths.

Tests under elastic/elreal/conversion/string.cpp gain three new cases
(elreal("-0"), elreal("-0.0"), elreal("-0/7") all asserted via
std::signbit) so the contract is locked in.

Trivial style cleanup in elastic/elreal/conversion/native_types.cpp:
std::signbit(...) != true -> !std::signbit(...).

Declining one CodeRabbit nitpick (whitespace-wrapped tokens like
"  +inf  "): the parser claims leading-whitespace-only support
(matching ereal's parse() in include/sw/universal/number/ereal/
ereal_impl.hpp:327). Trailing-whitespace handling is not part of the
contract on either type; adding it without ereal also adopting it
would create an asymmetric API surface. Left as future work if a
consumer needs it on both types.

Verified clean under gcc 13 and clang.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Ravenwater

Copy link
Copy Markdown
Contributor Author

Review status after CodeRabbit's first pass (commit 3ead8ea):

Finding Verdict Action
elreal_impl.hpp rational num/den overflow valid Fixed: num * 10 + digit > LLONG_MAX guard before each multiply
elreal_impl.hpp decimal mantissa overflow valid Fixed: same guard pattern
elreal_impl.hpp LLONG_MIN UB on -p valid (defensive) Fixed: replaced magnitude computation with unsigned long long. With the mantissa overflow guard above, the UB path is now unreachable through the parser, but the unsigned form protects any future direct caller of the rational ctor.
string.cpp "-0" signed-zero test valid Discovered that the parser was not preserving the sign of -0, contradicting the elreal(-0.0) round-trip behavior already in place. Fixed in both the rational and decimal/scientific paths; three new assertions cover "-0", "-0.0", "-0/7".
native_types.cpp != true style valid (trivial) Fixed: !std::signbit(...)
string.cpp whitespace-wrapped tokens (" +inf ") declining The parser claims leading-whitespace-only support, matching ereal::parse() (include/sw/universal/number/ereal/ereal_impl.hpp:327). Trailing whitespace is not part of the contract on either type; adopting it asymmetrically would diverge the API surface. Left as a future enhancement if a consumer needs it on both.

Validated under gcc 13 and clang; all four elreal_* binaries PASS.

@coderabbitai coderabbitai Bot left a comment

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.

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 `@include/sw/universal/number/elreal/elreal_impl.hpp`:
- Around line 522-524: The loop that parses digits currently returns false on
mantissa overflow; instead record the overflow (e.g., set a bool like
mantissa_overflow = true) and continue scanning remaining characters to fully
validate the literal without updating mantissa further, so you don't mutate
mantissa when it would overflow. Keep the overflow detection using the same
check around (std::numeric_limits<long long>::max() - digit) / 10, but when it
trips simply flip the flag and skip the mantissa update rather than returning
false; this allows the later fallback logic (the existing code around the
current fallback at lines ~596-603 that builds an approximate leading double) to
run and produce the faithful approximate value.
🪄 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: e8075744-7af7-4b48-8130-c2a0f38b38fe

📥 Commits

Reviewing files that changed from the base of the PR and between 5d822c7 and 3ead8ea.

📒 Files selected for processing (3)
  • elastic/elreal/conversion/native_types.cpp
  • elastic/elreal/conversion/string.cpp
  • include/sw/universal/number/elreal/elreal_impl.hpp

Comment thread include/sw/universal/number/elreal/elreal_impl.hpp Outdated
Address CodeRabbit second-round finding on PR #884.

The previous commit added overflow guards on the mantissa accumulator
that returned false immediately, which had an unintended consequence:
valid scientific literals with 20+ significant digits (e.g.,
"12345678901234567890e5") silently parsed as canonical zero through
the std::string ctor.

The code already had an `overflowed` flag and a fallback path for the
q_pow scaling overflow case, but the mantissa overflow was hitting
return false before reaching the fallback.

Fix: on mantissa overflow, set a mantissa_overflow flag and keep
scanning to validate the rest of the literal (rejecting on invalid
characters, exhausting the exponent, etc.). After the parse loop
completes, if mantissa_overflow is set, use std::stod() on the
original input string to get the correctly-rounded leading double
and construct the elreal from that. Exact-rational refinement past
depth 0 is unavailable in this overflow path -- a documented
Phase B limitation matching the existing q_pow overflow fallback.

Tests in elastic/elreal/conversion/string.cpp gain two cases:
- "12345678901234567890" (20-digit integer mantissa)
- "1.234567890123456789012345e5" (25-digit scientific mantissa)

Both must produce non-zero results matching std::stod on the same
string. Verified clean under gcc 13 and clang.

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

@coderabbitai coderabbitai Bot left a comment

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.

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 `@include/sw/universal/number/elreal/elreal_impl.hpp`:
- Around line 518-535: The exponent parsing currently accumulates into int
exponent without guarding against signed overflow; add a bool exponent_overflow
(like mantissa_overflow) and, inside the exponent digit loop that updates
exponent = exponent * 10 + (c - '0'), check before updating whether exponent >
(std::numeric_limits<int>::max() - digit) / 10 and set exponent_overflow instead
of performing the update; stop updating exponent once overflow would occur but
continue scanning, and ensure the existing fallback that calls std::stod(str) is
triggered when (mantissa_overflow || exponent_overflow) rather than only
mantissa_overflow so large exponents route to the fallback safely.
🪄 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: fc1fcfed-68e5-4c3f-af9a-db393d88aec9

📥 Commits

Reviewing files that changed from the base of the PR and between 3ead8ea and 3016552.

📒 Files selected for processing (2)
  • elastic/elreal/conversion/string.cpp
  • include/sw/universal/number/elreal/elreal_impl.hpp

Comment thread include/sw/universal/number/elreal/elreal_impl.hpp
… fallback

Address CodeRabbit third-round finding on PR #884.

The exponent accumulation in parse_into was vulnerable to signed integer
overflow UB for adversarial inputs like "1e9999999999" -- the second-round
mantissa fix only covered mantissa overflow, leaving the exponent loop
exposed. CodeRabbit caught this in their third pass.

Two changes:

1. Mirror the mantissa overflow guard on the exponent loop. Track
   exponent_overflow with the same `exp > (INT_MAX - digit) / 10` test
   before each multiply-add. Stop updating exponent on overflow but
   keep scanning to validate format. Route `mantissa_overflow ||
   exponent_overflow` to the existing fallback.

2. Switch the fallback from std::stod to std::strtod. std::stod throws
   std::out_of_range when the value is unrepresentable as double
   (e.g. for "1e9999999999"); my catch swallowed the exception and
   returned false, yielding canonical zero through the std::string ctor.
   std::strtod sets errno = ERANGE and returns +/-HUGE_VAL (overflow)
   or 0.0 (underflow), which matches IEEE-754 saturation semantics --
   the right behavior for inputs the rational track cannot represent
   anyway.

Tests in elastic/elreal/conversion/string.cpp gain two cases:
- "1e9999999999" must saturate to +inf (not silently zero)
- "1e-9999999999" must underflow to zero

Caught the std::stod-throws issue during local testing of the first
attempt -- the initial fix used std::stod and failed under both gcc
and clang. Switching to std::strtod made both compilers green.

Verified all four elreal_* binaries PASS on gcc 13 and clang.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants