This is an automated email from the ASF dual-hosted git repository.

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-nanoarrow.git


The following commit(s) were added to refs/heads/main by this push:
     new e5f3d0a5 fix: preserve half-float subnormal values (#962)
e5f3d0a5 is described below

commit e5f3d0a55d5576386359835f7bf052f983d1161f
Author: Nathan Chandrasekar <[email protected]>
AuthorDate: Fri Oct 16 10:27:26 2026 -0400

    fix: preserve half-float subnormal values (#962)
    
    The half-float getter returned incorrect values for nonzero subnormals,
    and the appender stored them as zero. Normalize subnormal decoding and
    restore the implicit leading bit when encoding.
    
    Regression and all 63,488 finite binary16 encodings pass. Serial CTest:
    371 passed, 16 optional compression skips.
    
    AI-assisted, reviewed by me.
    
    Fixes #961
    
    — [ChipLab](https://chiplab.donmakino.com/findings#clab-2026-867)
---
 src/nanoarrow/common/inline_buffer.h | 13 ++++++++++++-
 src/nanoarrow/common/utils_test.cc   | 23 +++++++++++++++++++++++
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/src/nanoarrow/common/inline_buffer.h 
b/src/nanoarrow/common/inline_buffer.h
index 929fe815..bd74afb4 100644
--- a/src/nanoarrow/common/inline_buffer.h
+++ b/src/nanoarrow/common/inline_buffer.h
@@ -117,14 +117,16 @@ static inline uint16_t ArrowFloatToHalfFloat(float value) 
{
 
   if (exp == 0) {
     res = 0;
+    fc = 0;
   } else if (exp == 0xff) {
     res = 0x1f;
   } else if (res > 0x1e) {
     res = 0x1f;
     fc = 0;
   } else if (res < 0x01) {
+    // Restore the implicit leading bit and shift for a subnormal exponent.
+    fc = res < -9 ? 0 : (uint16_t)((fc | 0x400) >> (1 - res));
     res = 0;
-    fc = 0;
   }
 
   return (uint16_t)((sn << 15) | (uint16_t)(res << 10) | fc);
@@ -140,6 +142,15 @@ static inline float ArrowHalfFloatToFloat(uint16_t value) {
 
   if (exp == 0) {
     res = 0;
+    if (fc != 0) {
+      // Normalize the subnormal significand before rebiasing its exponent.
+      res = 127 - 14;
+      while ((fc & 0x400) == 0) {
+        fc <<= 1;
+        res--;
+      }
+      fc &= 0x3ff;
+    }
   } else if (exp == 0x1f) {
     res = 0xff;
   }
diff --git a/src/nanoarrow/common/utils_test.cc 
b/src/nanoarrow/common/utils_test.cc
index d4e43ad5..0d52d0f9 100644
--- a/src/nanoarrow/common/utils_test.cc
+++ b/src/nanoarrow/common/utils_test.cc
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <cmath>
 #include <cstring>
 #include <string>
 
@@ -759,6 +760,28 @@ TEST(HalfFloatTest, FloatAndHalfFloatRoundTrip) {
   }
 }
 
+TEST(HalfFloatTest, FloatAndHalfFloatSubnormals) {
+  // Include signed zeros and the smallest normals at the subnormal boundaries.
+  for (uint16_t bits = 0; bits <= 0x400; bits++) {
+    SCOPED_TRACE(bits);
+    float expected = std::ldexp(static_cast<float>(bits), -24);
+    EXPECT_EQ(ArrowHalfFloatToFloat(bits), expected);
+    EXPECT_EQ(ArrowHalfFloatToFloat(bits | 0x8000), -expected);
+    EXPECT_EQ(ArrowFloatToHalfFloat(expected), bits);
+    EXPECT_EQ(ArrowFloatToHalfFloat(-expected), bits | 0x8000);
+  }
+
+  EXPECT_FALSE(std::signbit(ArrowHalfFloatToFloat(0x0000)));
+  EXPECT_TRUE(std::signbit(ArrowHalfFloatToFloat(0x8000)));
+
+  float underflow[] = {std::ldexp(1.0f, -25), std::ldexp(1.0f, -126),
+                       std::ldexp(1.5f, -127)};
+  for (float value : underflow) {
+    EXPECT_EQ(ArrowFloatToHalfFloat(value), 0x0000);
+    EXPECT_EQ(ArrowFloatToHalfFloat(-value), 0x8000);
+  }
+}
+
 TEST(UtilsTest, ArrowResolveChunk64Test) {
   int64_t offsets[] = {0, 2, 3, 6};
   int64_t n_offsets = 4;

Reply via email to