Skip to content

feat(elreal): Phase 1 -- block<FpType> representation#934

Merged
Ravenwater merged 5 commits into
mainfrom
feat/issue-925-elreal-block
May 23, 2026
Merged

feat(elreal): Phase 1 -- block<FpType> representation#934
Ravenwater merged 5 commits into
mainfrom
feat/issue-925-elreal-block

Conversation

@Ravenwater

@Ravenwater Ravenwater commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 of the McCleeary LFPERA reimplementation (epic #923). Foundational block<FpType> type with the int32_t exp_offset escape multiplier, 0-overlap predicate, pretty-printers, and a sweep test suite over six FpType variants.

Topic Detail
Block layout (FpType v, int32_t exp_offset); trivial copyable, no in-class init
Combined exponent E(b) = scale_of(v) + exp_offset * 2^E_FpType (int64, won't overflow)
McCleeary k numeric_limits<FpType>::digits (matches dissertation's block-vector width)
FpType sweep float, double, half, bfloat16, cfloat<24,5>, cfloat<32,8>
0-overlap predicate E(b1) >= E(b2) + k; zero blocks trivially 0-overlap any block

What's in / what's out

In Phase 1:

  • block<FpType> struct + accessors (sign(), scale_of_v(), exponent(), value_as<T>())
  • Predicates (is_zero_block(), is_normalised())
  • Free function zero_overlap(b1, b2)
  • Pretty-printers (to_binary, to_hex, color_print)
  • 6 sweep tests parameterised over FpType
  • CMake wiring: new UNIVERSAL_BUILD_NUMBER_ELREALS option, cascade into ELASTICS and CI_LITE

Deferred:

k values observed

FpType k E exp_step
float 24 8 256
double 53 11 2048
half (cfloat<16,5>) 11 5 32
bfloat16 7 8 256
cfloat<24,5> 19 5 32
cfloat<32,8> 24 8 256

Note on bfloat16: Universal's numeric_limits<bfloat16>::digits reports 7 (just the explicit fraction bits); the dissertation-consistent k would be 8 (including the hidden bit). Recorded here for visibility; not a Phase 1 blocker.

Test results

Target gcc 13.3 clang 18.1
el_block_construction PASS PASS
el_block_accessors PASS PASS
el_block_predicates PASS PASS
el_block_zero_overlap PASS PASS
el_block_exp_offset PASS PASS
el_block_printers PASS PASS

Test plan

  • gcc + clang local builds clean and all tests pass
  • Fast CI (gcc + clang CI_LITE)
  • Promote to ready (gh pr ready) when satisfied
  • Full CI on ready

Resolves #925

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced Phase‑1 "elreal" number system with a block<> numeric primitive, human‑readable binary/hex formatting, and a zero‑overlap predicate.
  • Tests

    • Added test suites covering block construction, accessors, exponent/offset behavior, predicates, printers, and overlap detection across multiple floating‑point types.
  • Build

    • New build option to enable elreal; CI presets updated to enable it by default.
  • Documentation

    • Added analysis documenting current implementation behavior and suggested remediation paths.

Review Change Stack

Phase 1 of the McCleeary LFPERA reimplementation (epic #923). Lands the
foundational `block<FpType>` type with the int32_t `exp_offset` escape
multiplier, the 0-overlap predicate, pretty-printers, and a sweep test
suite parameterised over float, double, half, bfloat16, cfloat<24,5>,
and cfloat<32,8>.

What `block<FpType>` holds
- `FpType v`: sign + biased exponent + k-bit significand packed into the
  host FP type.
- `int32_t exp_offset`: multiplier in units of `2^E_FpType` so a single
  block can carry an exponent far outside the host type's hardware range,
  which Phase 7 transcendentals will need.

Effective value `v * 2^(exp_offset * 2^E_FpType)`.
Combined exponent `E(b) = scale_of(v) + exp_offset * 2^E_FpType`.
McCleeary's k = numeric_limits<FpType>::digits.

Files
- `include/sw/universal/number/elreal/exp_field_width.hpp`: trait that
  yields E for float (8), double (11), bfloat16 (8), cfloat<N,E> (E).
- `include/sw/universal/number/elreal/block.hpp`: block<FpType> struct
  plus `zero_overlap(b1, b2)` free function. Trivial layout (no in-class
  initialisers), branches on `has_universal_fp_api_v<FpType>` to pick
  between Universal's .sign()/.scale() and native std::signbit/std::ilogb.
- `include/sw/universal/number/elreal/block_manipulators.hpp`: to_binary,
  to_hex, color_print. to_hex memcpys the bit pattern for native float
  and double; for Universal wrappers it falls back to the human-readable
  form (to avoid cross-header churn -- richer rendering can come later).
- `include/sw/universal/number/elreal/elreal.hpp`: umbrella header.
- `include/sw/universal/number/elreal/elreal_fwd.hpp`: forward decl.

Tests under `elastic/elreal/block/`
- construction.cpp: trivial-copyable + trivial-destructible assertions;
  value and value+offset construction; copy semantics; compile-time
  constants (k, E, exp_step).
- accessors.cpp: sign() on +/-1.5; scale_of_v() on 1, 2, 0.5;
  exponent() with offset; value_as<double>() roundtrip.
- predicates.cpp: is_zero_block on 0, on 0-with-offset, on nonzero;
  is_normalised on normal and zero.
- zero_overlap.cpp: boundary at E(b2)=-k; in-the-gap violation at -k+1;
  wide gap; zero blocks trivially 0-overlap; cross-offset gap;
  reversed pair fails.
- exp_offset.cpp: offset 0, +/-1, +100, INT32_MAX; verifies int64
  arithmetic does not overflow.
- printers.cpp: smoke tests; non-empty output for every supported FpType.

Build wiring
- New CMake option `UNIVERSAL_BUILD_NUMBER_ELREALS` (mirrors EREALS).
- Cascade into the ELASTICS umbrella.
- Cascade into CI_LITE so portability is validated on every PR.
- `elastic/elreal/CMakeLists.txt` registers the block tests via
  compile_all under `el_block_*`.

Validation
- gcc 13.3: all 6 tests PASS.
- clang 18.1: all 6 tests PASS.
- k values: float=24, double=53, half=11, bfloat16=7 (Universal
  numeric_limits quirk: dissertation k would be 8), cfloat<24,5>=19,
  cfloat<32,8>=24.

Out of scope for Phase 1
- ZBCL co-list construction (Phase 2, #926)
- twoSum / twoMult / twoDiv on blocks (Phase 3, #927)
- Stream-level arithmetic (Phase 4+)

Resolves #925

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

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 42f16ed2-8ac1-47b3-8177-8eb3c8fa9344

📥 Commits

Reviewing files that changed from the base of the PR and between a1d0e61 and de271ff.

📒 Files selected for processing (1)
  • docs/bugs/elreal-implementation.md

📝 Walkthrough

Walkthrough

Phase‑1 elreal: adds exponent-field traits, core block<FpType> representation and zero‑overlap predicate, pretty‑printers, umbrella headers, CMake wiring, parameterized block tests, and a design/bug analysis document.

Changes

elreal Phase 1: block Foundation

Layer / File(s) Summary
Build configuration and project wiring
CMakeLists.txt, elastic/elreal/CMakeLists.txt
New UNIVERSAL_BUILD_NUMBER_ELREALS option; presets UNIVERSAL_BUILD_CI_LITE and UNIVERSAL_BUILD_NUMBER_ELASTICS enable it; elastic/elreal added and block tests globbed into el_block.
Exponent field width traits
include/sw/universal/number/elreal/exp_field_width.hpp
exp_field_width<T> primary template and specializations for float, double, conditional long double, bfloat16, and cfloat; exp_field_width_v and shift-safe exp_step_v added.
Block data structure and operations
include/sw/universal/number/elreal/block.hpp
has_universal_fp_api detection trait and block<FpType> storing v and int32_t exp_offset with sign(), scale_of_v(), exponent(), is_zero_block(), is_normalised(), value_as<T>(), and zero_overlap().
Block pretty-printers
include/sw/universal/number/elreal/block_manipulators.hpp
type_tag(), to_binary(), to_hex(), and color_print() implementations; float/double hex rendering and fallback to binary for other FP types.
Forward declarations and umbrella header
include/sw/universal/number/elreal/elreal_fwd.hpp, include/sw/universal/number/elreal/elreal.hpp
Adds forward declaration for block<FpType> and umbrella header wiring Phase‑1 components.
Block test suite
elastic/elreal/block/{accessors,construction,exp_offset,predicates,printers,zero_overlap}.cpp
Six test executables parameterised over {float, double, half, bfloat16, cfloat<24,5>, cfloat<32,8>} covering construction, accessors, exp_offset boundaries, zero-block/normalisation predicates, zero-overlap predicate, and pretty-printer smoke tests.
Design / bug analysis
docs/bugs/elreal-implementation.md
Detailed analysis contrasting McCleeary’s algorithm with the current implementation and outlining remediation options and issue updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

enhancement

Poem

🐇 I built a block with bits to spare,
An offset tucked with careful care.
Printers hum and tests will run,
Zero‑overlap says when we're done.
Phase one hops off toward the sun.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% 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 describes the main change: implementing Phase 1 of the elreal system with the foundational block representation type.
Linked Issues check ✅ Passed All acceptance criteria from issue #925 are met: block struct, accessors (sign, exponent, value_as), predicates (is_zero_block, is_normalised), zero_overlap function, pretty-printers (to_binary, to_hex, color_print), parameterized tests covering all required FpType variants, CMake wiring with UNIVERSAL_BUILD_NUMBER_ELREALS option, and verified builds under gcc/clang.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Phase 1 objectives: block representation, accessors, predicates, zero_overlap function, pretty-printers, comprehensive tests, and CMake configuration. No unrelated or out-of-scope changes are present.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-925-elreal-block

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

@Ravenwater Ravenwater self-assigned this May 23, 2026
@Ravenwater Ravenwater marked this pull request as ready for review May 23, 2026 00:26

@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 (2)
include/sw/universal/number/elreal/block_manipulators.hpp (1)

40-42: ⚡ Quick win

Prefer std::fabs for clarity when computing absolute value.

Line 41 manually negates abs_frac if it's negative. While this works, using std::fabs(abs_frac) or abs_frac = std::abs(abs_frac) is more idiomatic and clearer.

♻️ Proposed refactor for clarity
     double abs_frac = static_cast<double>(b.v) / std::ldexp(1.0, b.scale_of_v());
-    if (abs_frac < 0) abs_frac = -abs_frac;
+    abs_frac = std::fabs(abs_frac);
     s << " * " << std::scientific << std::setprecision(6) << abs_frac;
🤖 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 `@include/sw/universal/number/elreal/block_manipulators.hpp` around lines 40 -
42, The code computes abs_frac then manually negates it when negative; replace
the manual sign check with a call to the standard absolute function (e.g., use
std::fabs(abs_frac) or std::abs(abs_frac)) so the assignment to abs_frac uses
the standard library for clarity and intent; update the expression around the
variable abs_frac in the block that constructs the scientific string (the lines
around abs_frac = static_cast<double>(b.v) / std::ldexp(1.0, b.scale_of_v()) and
the subsequent sign handling) to use std::fabs/ std::abs instead of the if
(abs_frac < 0) abs_frac = -abs_frac; pattern.
elastic/elreal/block/predicates.cpp (1)

36-42: ⚡ Quick win

Add boundary checks for is_normalised() contract

Please add explicit negative cases for values below and above the normalized window (for example 0.75 and 2.0) so the test enforces the [1,2) invariant.

Proposed test additions
     // is_normalised on normal values
     B norm{ FpType{1.5}, 0 };
     if (!norm.is_normalised()) { std::cout << tag << " 1.5 not normalised\n"; ++nrFailures; }

+    // boundary/out-of-range checks for [1,2)
+    B below{ FpType{0.75}, 0 };
+    B above{ FpType{2.0}, 0 };
+    if (below.is_normalised()) { std::cout << tag << " 0.75 incorrectly normalised\n"; ++nrFailures; }
+    if (above.is_normalised()) { std::cout << tag << " 2.0 incorrectly normalised\n"; ++nrFailures; }
+
     // is_normalised on zero -> false
     if (zero.is_normalised()) { std::cout << tag << " zero claimed normalised\n"; ++nrFailures; }
🤖 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/block/predicates.cpp` around lines 36 - 42, Add explicit
negative test cases to enforce the [1,2) invariant by checking is_normalised()
returns false for values just below and just above the normalized window:
construct B test instances like B low{ FpType{0.75}, 0 } and B high{
FpType{2.0}, 0 } (matching the existing pattern using FpType and B), then assert
!low.is_normalised() and !high.is_normalised(), incrementing nrFailures and
printing the same tag-style message on failure; place these checks alongside the
existing norm and zero checks so the contract for is_normalised() is fully
validated.
🤖 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 `@elastic/elreal/block/accessors.cpp`:
- Around line 49-60: The floating comparison in the test using B (constructed
from FpType{3.25}) reads recovered = v.template value_as<double>() and compares
std::abs(recovered - host_stored) > 0.0 which is effectively exact-equality;
update the condition in the check that reports failures (the if that currently
uses std::abs(... ) > 0.0) to either perform a bit-pattern mismatch check using
recovered != host_stored (to catch true unequal doubles) or use a small epsilon
(e.g., compare std::abs(recovered - host_stored) > epsilon) appropriate for
FpType/host_stored to allow rounding differences, keeping the rest of the
failure reporting (tag, nrFailures increment) unchanged.

In `@include/sw/universal/number/elreal/block.hpp`:
- Around line 110-126: The is_normalised() logic must enforce the documented
invariant that |v| is in [1,2); update both branches to test the absolute value
range instead of (or in addition to) fpclassify/scale: keep the early
is_zero_block() check, compute abs_v from v (using v.sign() ? -v : v) and return
(abs_v >= FpType{1} && abs_v < FpType{2}) for the universal FpType path (and do
the same for the native path instead of relying only on std::fpclassify(v) ==
FP_NORMAL); this ensures is_normalised() only accepts values in [1,2) and
rejects 0, subnormals and values like 0.75 or 3.0.

In `@include/sw/universal/number/elreal/exp_field_width.hpp`:
- Around line 42-44: exp_step_v currently does an unchecked left shift which is
undefined for exp_field_width_v<T> >= 63; change its definition to guard the
shift: compute the step as (exp_field_width_v<T> < 63 ?
static_cast<std::int64_t>(1) << exp_field_width_v<T> :
std::numeric_limits<std::int64_t>::max()) and include <limits> if needed, so
exp_step_v<T> never performs an invalid shift; reference the template variable
exp_step_v and exp_field_width_v<T> when making this change.

---

Nitpick comments:
In `@elastic/elreal/block/predicates.cpp`:
- Around line 36-42: Add explicit negative test cases to enforce the [1,2)
invariant by checking is_normalised() returns false for values just below and
just above the normalized window: construct B test instances like B low{
FpType{0.75}, 0 } and B high{ FpType{2.0}, 0 } (matching the existing pattern
using FpType and B), then assert !low.is_normalised() and !high.is_normalised(),
incrementing nrFailures and printing the same tag-style message on failure;
place these checks alongside the existing norm and zero checks so the contract
for is_normalised() is fully validated.

In `@include/sw/universal/number/elreal/block_manipulators.hpp`:
- Around line 40-42: The code computes abs_frac then manually negates it when
negative; replace the manual sign check with a call to the standard absolute
function (e.g., use std::fabs(abs_frac) or std::abs(abs_frac)) so the assignment
to abs_frac uses the standard library for clarity and intent; update the
expression around the variable abs_frac in the block that constructs the
scientific string (the lines around abs_frac = static_cast<double>(b.v) /
std::ldexp(1.0, b.scale_of_v()) and the subsequent sign handling) to use
std::fabs/ std::abs instead of the if (abs_frac < 0) abs_frac = -abs_frac;
pattern.
🪄 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: 4de26393-296a-4f47-bc11-078da6f63797

📥 Commits

Reviewing files that changed from the base of the PR and between 85cb5e5 and 1634e2d.

📒 Files selected for processing (13)
  • CMakeLists.txt
  • elastic/elreal/CMakeLists.txt
  • elastic/elreal/block/accessors.cpp
  • elastic/elreal/block/construction.cpp
  • elastic/elreal/block/exp_offset.cpp
  • elastic/elreal/block/predicates.cpp
  • elastic/elreal/block/printers.cpp
  • elastic/elreal/block/zero_overlap.cpp
  • include/sw/universal/number/elreal/block.hpp
  • include/sw/universal/number/elreal/block_manipulators.hpp
  • include/sw/universal/number/elreal/elreal.hpp
  • include/sw/universal/number/elreal/elreal_fwd.hpp
  • include/sw/universal/number/elreal/exp_field_width.hpp

Comment thread elastic/elreal/block/accessors.cpp Outdated
Comment on lines +110 to +126
// is_normalised(): for IEEE-style FpTypes the leading significand bit must be
// set; equivalently, |v| in [1, 2). McCleeary blocks must be normalised so
// 0-overlap accounting holds.
constexpr bool is_normalised() const noexcept {
if (is_zero_block()) return false;
// For all supported FpType (native and Universal), ilogb / scale returns
// a valid integer for normalised values. Subnormals are NOT considered
// normalised here.
if constexpr (has_universal_fp_api_v<FpType>) {
// Universal cfloat / bfloat16: scale() works for both normal and
// subnormal values; check the value range explicitly.
FpType abs_v = v.sign() ? -v : v;
return abs_v >= FpType{1} || (v.scale() >= std::numeric_limits<FpType>::min_exponent - 1);
} else {
int cls = std::fpclassify(v);
return cls == FP_NORMAL;
}

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 | 🟠 Major | ⚡ Quick win

is_normalised() does not match documented block invariant

The implementation currently accepts values outside [1, 2) (for example 0.75 and 3.0), which conflicts with the stated normalized definition and can corrupt overlap assumptions.

Proposed fix
     constexpr bool is_normalised() const noexcept {
         if (is_zero_block()) return false;
-        // For all supported FpType (native and Universal), ilogb / scale returns
-        // a valid integer for normalised values. Subnormals are NOT considered
-        // normalised here.
         if constexpr (has_universal_fp_api_v<FpType>) {
-            // Universal cfloat / bfloat16: scale() works for both normal and
-            // subnormal values; check the value range explicitly.
             FpType abs_v = v.sign() ? -v : v;
-            return abs_v >= FpType{1} || (v.scale() >= std::numeric_limits<FpType>::min_exponent - 1);
+            return abs_v >= FpType{1} && abs_v < FpType{2};
         } else {
-            int cls = std::fpclassify(v);
-            return cls == FP_NORMAL;
+            if (!std::isfinite(v)) return false;
+            const FpType abs_v = std::fabs(v);
+            return abs_v >= FpType{1} && abs_v < FpType{2};
         }
     }
📝 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
// is_normalised(): for IEEE-style FpTypes the leading significand bit must be
// set; equivalently, |v| in [1, 2). McCleeary blocks must be normalised so
// 0-overlap accounting holds.
constexpr bool is_normalised() const noexcept {
if (is_zero_block()) return false;
// For all supported FpType (native and Universal), ilogb / scale returns
// a valid integer for normalised values. Subnormals are NOT considered
// normalised here.
if constexpr (has_universal_fp_api_v<FpType>) {
// Universal cfloat / bfloat16: scale() works for both normal and
// subnormal values; check the value range explicitly.
FpType abs_v = v.sign() ? -v : v;
return abs_v >= FpType{1} || (v.scale() >= std::numeric_limits<FpType>::min_exponent - 1);
} else {
int cls = std::fpclassify(v);
return cls == FP_NORMAL;
}
// is_normalised(): for IEEE-style FpTypes the leading significand bit must be
// set; equivalently, |v| in [1, 2). McCleeary blocks must be normalised so
// 0-overlap accounting holds.
constexpr bool is_normalised() const noexcept {
if (is_zero_block()) return false;
if constexpr (has_universal_fp_api_v<FpType>) {
FpType abs_v = v.sign() ? -v : v;
return abs_v >= FpType{1} && abs_v < FpType{2};
} else {
if (!std::isfinite(v)) return false;
const FpType abs_v = std::fabs(v);
return abs_v >= FpType{1} && abs_v < FpType{2};
}
}
🤖 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 `@include/sw/universal/number/elreal/block.hpp` around lines 110 - 126, The
is_normalised() logic must enforce the documented invariant that |v| is in
[1,2); update both branches to test the absolute value range instead of (or in
addition to) fpclassify/scale: keep the early is_zero_block() check, compute
abs_v from v (using v.sign() ? -v : v) and return (abs_v >= FpType{1} && abs_v <
FpType{2}) for the universal FpType path (and do the same for the native path
instead of relying only on std::fpclassify(v) == FP_NORMAL); this ensures
is_normalised() only accepts values in [1,2) and rejects 0, subnormals and
values like 0.75 or 3.0.

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

coveralls commented May 23, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 26320320824

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.03%) to 84.252%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (55 of 56 lines covered, 98.21%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
include/sw/universal/number/elreal/block.hpp 25 24 96.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 55785
Covered Lines: 47000
Line Coverage: 84.25%
Coverage Strength: 5276962.02 hits per line

💛 - Coveralls

CodeRabbit findings
- accessors.cpp: value_as<double>() comparison was effectively exact
  equality via `abs(diff) > 0.0`; switched to direct `!=` to make intent
  clear (value_as with offset=0 IS just a cast, no rounding expected).
- block.hpp is_normalised(): the docstring claimed "|v| in [1,2)" which
  is wrong -- McCleeary normalisation is about the hidden bit being set
  (IEEE FP_NORMAL), independent of magnitude. 3.0 (= 1.5*2^1) IS
  normalised. Fixed the docstring and made the Universal-type branch use
  isnormal() via a C++20 requires-expression, falling back to a
  numeric_limits<>::min() check for types like bfloat16 that lack
  isnormal().
- predicates.cpp: added 0.75 and 3.0 as positive cases (both normal IEEE
  values, both must pass) and a subnormal case (must fail). Guards the
  subnormal test with has_denorm == denorm_present so it skips on
  formats without subnormal support.
- block_manipulators.hpp: replaced the manual `if (x < 0) x = -x` with
  std::fabs() per CodeRabbit's nitpick.
- exp_field_width.hpp: guarded the `1 << E` against undefined behaviour
  for hypothetical FpType with E >= 63 (none today, defensive only).

Codacy finding
- Cppcheck flagged `block::E` as unused (false positive -- it's a public
  constexpr referenced by the test). Restructured `block::exp_step` to
  compute `1 << E` locally instead of pulling from `exp_step_v<FpType>`,
  so Cppcheck sees the dependency. Same guard against E >= 63.

Validation
- gcc 13.3: all 6 tests still PASS (now with stricter coverage).
- clang 18.1: all 6 tests still PASS.

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.

🧹 Nitpick comments (1)
include/sw/universal/number/elreal/block_manipulators.hpp (1)

67-74: 💤 Low value

Early return bypasses exp_offset suffix for Universal types.

When FpType is a Universal wrapper type (not float or double), to_hex returns to_binary(b) directly at line 69, which does include the exp_offset suffix. So this is actually consistent, since to_binary handles it. However, the comment on line 68 says "defer to the human-readable form" without mentioning that exp_offset is handled there.

Consider adding a brief comment clarifying that to_binary already appends exp_offset, or this could confuse future maintainers reading lines 71-73.

🤖 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 `@include/sw/universal/number/elreal/block_manipulators.hpp` around lines 67 -
74, The early return in to_hex that calls to_binary(b) for Universal wrapper
FpType bypasses the later exp_offset suffix code, which is fine because
to_binary already appends the exp_offset; update the to_hex implementation by
replacing the vague comment "// Universal wrapper types: defer to the
human-readable form for now." with a clear note that to_binary(b) includes the
exp_offset suffix (mentioning b.exp_offset) so future readers understand why the
subsequent exp_offset handling is skipped, or alternatively move the
exp_offset-appending logic into a shared helper used by both to_hex and
to_binary if you prefer a single source of truth.
🤖 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.

Nitpick comments:
In `@include/sw/universal/number/elreal/block_manipulators.hpp`:
- Around line 67-74: The early return in to_hex that calls to_binary(b) for
Universal wrapper FpType bypasses the later exp_offset suffix code, which is
fine because to_binary already appends the exp_offset; update the to_hex
implementation by replacing the vague comment "// Universal wrapper types: defer
to the human-readable form for now." with a clear note that to_binary(b)
includes the exp_offset suffix (mentioning b.exp_offset) so future readers
understand why the subsequent exp_offset handling is skipped, or alternatively
move the exp_offset-appending logic into a shared helper used by both to_hex and
to_binary if you prefer a single source of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd8c04a0-74ff-4a09-9977-146614e5d198

📥 Commits

Reviewing files that changed from the base of the PR and between 1634e2d and 69c8b69.

📒 Files selected for processing (5)
  • elastic/elreal/block/accessors.cpp
  • elastic/elreal/block/predicates.cpp
  • include/sw/universal/number/elreal/block.hpp
  • include/sw/universal/number/elreal/block_manipulators.hpp
  • include/sw/universal/number/elreal/exp_field_width.hpp
✅ Files skipped from review due to trivial changes (1)
  • elastic/elreal/block/predicates.cpp

Ravenwater and others added 2 commits May 22, 2026 21:32
CodeRabbit nitpick on PR #934: the comment in to_hex's Universal-wrapper
branch said "defer to the human-readable form for now" without
explaining that the early return is intentional because to_binary
already appends the " ^ <exp_offset>" suffix. Expanded the comment so
future readers don't add a redundant suffix to the bypass path.

Comment-only change; no functional impact.

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.

🧹 Nitpick comments (4)
include/sw/universal/number/elreal/block_manipulators.hpp (4)

31-31: 💤 Low value

Consider [[maybe_unused]] for unused parameters.

The commented-out parameter /*nibbleMarker*/ suppresses unused-parameter warnings. The modern C++17 approach is [[maybe_unused]] bool nibbleMarker. Both are acceptable; this is a stylistic choice.

🤖 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 `@include/sw/universal/number/elreal/block_manipulators.hpp` at line 31, The
parameter suppression in function to_binary(const block<FpType>& b, bool
/*nibbleMarker*/ = false) should use the C++ attribute instead of a comment;
update the signature for the function to_binary to declare the parameter as
[[maybe_unused]] bool nibbleMarker = false (or equivalent C++17 attribute use)
so the parameter name remains visible and unused-parameter warnings are
suppressed; keep the default value and do not change other semantics.

53-55: 💤 Low value

Consider [[maybe_unused]] for unused parameters.

Similar to to_binary, the /*nibbleMarker*/ comment suppresses warnings. The C++17 [[maybe_unused]] attribute is more idiomatic but the current approach is acceptable.

🤖 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 `@include/sw/universal/number/elreal/block_manipulators.hpp` around lines 53 -
55, The parameter comment trick in the to_hex function signature should be
replaced with the C++17 [[maybe_unused]] attribute to mark nibbleMarker as
intentionally unused; update the function declaration inline std::string
to_hex(const block<FpType>& b, [[maybe_unused]] bool nibbleMarker = false, bool
hexPrefix = true) so the compiler knows the parameter is unused without a
comment and keeps the default behavior and signature semantics intact.

21-26: 💤 Low value

Consider implementation-dependent typeid().name() output.

The mangled name from typeid(FpType).name() varies by compiler (e.g., "f" vs "float"). For diagnostic consistency, you might prefer a traits-based approach or explicit specializations, but the current implementation is acceptable for debugging purposes.

🤖 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 `@include/sw/universal/number/elreal/block_manipulators.hpp` around lines 21 -
26, The current type_tag(block<FpType>) uses implementation-dependent
typeid(FpType).name(); replace this with a portable type-name trait: create a
template type_name<T>() (or struct type_name<T>::get()) with explicit
specializations for the common FpType candidates (e.g., float, double, long
double, int types) and fallback to a generic demangled or "unknown" string, then
change the type_tag implementation to use type_name<FpType>() instead of
typeid(FpType).name() so diagnostics are consistent across compilers.

40-41: ⚖️ Poor tradeoff

Potential edge cases in fraction extraction with extreme exponents.

Line 40 computes the fraction as fabs(double(b.v) / ldexp(1.0, b.scale_of_v())). For extreme scale_of_v() values (e.g., > 1023 or < -1074 for double), ldexp may return infinity or zero, leading to 0.0 or inf in the output fraction. Additionally, converting wide FpTypes to double may lose precision.

For Phase 1 diagnostic output this is likely acceptable, but consider guarding against overflow or documenting the limitation.

🤖 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 `@include/sw/universal/number/elreal/block_manipulators.hpp` around lines 40 -
41, The current fraction computation using abs_frac =
fabs(static_cast<double>(b.v) / std::ldexp(1.0, b.scale_of_v())) can produce 0.0
or inf for extreme b.scale_of_v() values and loses precision converting b.v to
double; before calling std::ldexp(1.0, b.scale_of_v()) in this expression, guard
by checking the exponent against double limits
(std::numeric_limits<double>::max_exponent and min_exponent) or perform the
scaling in long double, then validate the result with std::isfinite; if the
scaled divisor is non-finite, set abs_frac to a safe sentinel (e.g., 0.0 or NAN)
or emit a brief diagnostic note in the output so Phase 1 remains robust, and
update the computation to use long double for intermediate (e.g., use
std::ldexp((long double)1.0, b.scale_of_v()) and long double division) to reduce
precision loss when converting b.v.
🤖 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.

Nitpick comments:
In `@include/sw/universal/number/elreal/block_manipulators.hpp`:
- Line 31: The parameter suppression in function to_binary(const block<FpType>&
b, bool /*nibbleMarker*/ = false) should use the C++ attribute instead of a
comment; update the signature for the function to_binary to declare the
parameter as [[maybe_unused]] bool nibbleMarker = false (or equivalent C++17
attribute use) so the parameter name remains visible and unused-parameter
warnings are suppressed; keep the default value and do not change other
semantics.
- Around line 53-55: The parameter comment trick in the to_hex function
signature should be replaced with the C++17 [[maybe_unused]] attribute to mark
nibbleMarker as intentionally unused; update the function declaration inline
std::string to_hex(const block<FpType>& b, [[maybe_unused]] bool nibbleMarker =
false, bool hexPrefix = true) so the compiler knows the parameter is unused
without a comment and keeps the default behavior and signature semantics intact.
- Around line 21-26: The current type_tag(block<FpType>) uses
implementation-dependent typeid(FpType).name(); replace this with a portable
type-name trait: create a template type_name<T>() (or struct
type_name<T>::get()) with explicit specializations for the common FpType
candidates (e.g., float, double, long double, int types) and fallback to a
generic demangled or "unknown" string, then change the type_tag implementation
to use type_name<FpType>() instead of typeid(FpType).name() so diagnostics are
consistent across compilers.
- Around line 40-41: The current fraction computation using abs_frac =
fabs(static_cast<double>(b.v) / std::ldexp(1.0, b.scale_of_v())) can produce 0.0
or inf for extreme b.scale_of_v() values and loses precision converting b.v to
double; before calling std::ldexp(1.0, b.scale_of_v()) in this expression, guard
by checking the exponent against double limits
(std::numeric_limits<double>::max_exponent and min_exponent) or perform the
scaling in long double, then validate the result with std::isfinite; if the
scaled divisor is non-finite, set abs_frac to a safe sentinel (e.g., 0.0 or NAN)
or emit a brief diagnostic note in the output so Phase 1 remains robust, and
update the computation to use long double for intermediate (e.g., use
std::ldexp((long double)1.0, b.scale_of_v()) and long double division) to reduce
precision loss when converting b.v.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab783193-330a-46bd-aa7c-bd52acb0f5e8

📥 Commits

Reviewing files that changed from the base of the PR and between 69c8b69 and 23e7b7f.

📒 Files selected for processing (1)
  • include/sw/universal/number/elreal/block_manipulators.hpp

@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: 2

🤖 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 `@docs/bugs/elreal-implementation.md`:
- Line 20: Wrap any inline formulas that contain asterisks in backticks or use a
fenced code/math block so Markdown doesn't treat * as emphasis; specifically
update the line containing "(x * y)(n)  =  round(x(k) * y(k), n)" (and the
similar occurrence at the other noted location) to be enclosed in backticks or a
math block, ensuring the entire formula is treated as code/math rather than
parsed text.
- Around line 14-205: The Markdown uses non-ASCII glyphs (e.g. arrows like "→",
comparison symbols "≥"/"≤", box-drawing characters in the table, quote bars, and
special symbols like "imp" and "ZBCL_k") which violates the ASCII-only rule;
update the document by replacing these with plain ASCII equivalents (use "->"
for arrows, ">="/"<=" for comparisons, plain pipe/+- style ASCII table borders
instead of box-drawing, plain quotes or ">" for quote bars, and ensure
identifiers like ZBCL_k, imp, elreal, lazy_component_buffer, at(k), c_k, ulp
etc. remain ASCII and unaltered), scan the entire file for any remaining Unicode
and convert to ASCII-safe forms while preserving meaning and formatting.
🪄 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: e0b27467-c9b5-43dd-94af-b9007e484b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 23e7b7f and a1d0e61.

📒 Files selected for processing (2)
  • CMakeLists.txt
  • docs/bugs/elreal-implementation.md

Comment thread docs/bugs/elreal-implementation.md Outdated
Comment thread docs/bugs/elreal-implementation.md
CodeRabbit findings on PR #934 (commit a1d0e61 added the notes file):

1. Unicode characters violate the project's no-Unicode rule (CLAUDE.md).
   Replaced throughout: section sign -> Sec.; superscripts -> ^N; sigma
   -> sum; en/em dashes -> --; right-arrow -> ->; logical AND -> "and";
   filled-circle bullet -> *; left-half-block quote marker -> >;
   box-drawing characters -> proper Markdown table.

2. Markdown asterisks in inline pseudocode formulas (e.g.
   "(x * y)(n) = round(x(k) * y(k), n)") were being parsed as emphasis.
   Wrapped the two formula lines in a fenced code block so * is treated
   as literal punctuation.

3. While at it, fix the misspelled section heading "McCleearey
   algorithm" -> "McCleeary algorithm".

No content changes; cosmetics + ASCII conformance only.

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

elreal Phase 1: block<FpType> representation

2 participants