|
# pack('g')/unpack('g') single-precision NaN bug check
|
|
# Carsten Bormann cabo@tzi.org 2024-08-01
|
|
#
|
|
# Demonstrate that:
|
|
# - unpack("g") always sets the quiet bit to 1 in the Float result
|
|
# (location of bug not obvious to me)
|
|
# - pack("g") completely discards any actual NaN value and always packs the same bytes for a NaN
|
|
# ("bug as implemented" in VALUE_to_float)
|
|
|
|
class String
|
|
def hexs
|
|
bytes.map{|x| "%02x" % x}.join(" ")
|
|
end
|
|
end
|
|
|
|
def out(legend, *a)
|
|
puts legend + ": " + a.inspect
|
|
end
|
|
|
|
# The first three example suffice to show the problem
|
|
# The rest is cross-checking
|
|
EXAMPLES = [0x7fc00000, 0x7fffe000, 0x7fbfe000, 0x7ffff000, 0x7fbff000]
|
|
EXAMPLES.concat (EXAMPLES.map {|x| 0x80000000 | x}) # negative
|
|
|
|
def flip_upper_exponent_bit_first_byte(s)
|
|
s.setbyte(0, s.getbyte(0) ^ 0x40)
|
|
end
|
|
|
|
puts RUBY_DESCRIPTION
|
|
puts
|
|
|
|
EXAMPLES.each do |as_int|
|
|
as_bytes = [as_int].pack("N")
|
|
out "hex input for correct single NaN", "%x" % as_int, as_bytes.hexs
|
|
|
|
# Compute the NaN payload as a definite Float by flipping the upper exponent bit
|
|
# (Of course, unpack("g") works correctly with definite floats)
|
|
|
|
as_norm_bytes = [as_int ^ 0x40000000].pack("N")
|
|
out "- single nan payload normalized to 1.0..2.0", as_norm_bytes.hexs, as_norm_bytes.unpack("g").first
|
|
|
|
# For known good values, let's manually assemble the bit strings (knowing NaN exponent is all-ones)
|
|
# and cross-check (also checking pack('G') and unpack('G'))
|
|
|
|
single_bits = as_bytes.unpack("B32").first
|
|
double_bits = single_bits[0...9] + "1" * 3 + single_bits[9...32] + "0" * 29
|
|
correct_double_bytes = [double_bits].pack("B64")
|
|
correct_double_nan = correct_double_bytes.unpack("G").first
|
|
out "correct double_bytes, nan", correct_double_bytes.hexs, correct_double_nan
|
|
|
|
norm_correct_double_bytes = correct_double_bytes.dup
|
|
flip_upper_exponent_bit_first_byte(norm_correct_double_bytes)
|
|
norm_correct_double_nan = norm_correct_double_bytes.unpack("G").first
|
|
out "- correct double NaN payload normalized to 1.0..2.0", norm_correct_double_nan
|
|
|
|
cross_check_nan_bytes = [correct_double_nan].pack("G")
|
|
flip_upper_exponent_bit_first_byte(cross_check_nan_bytes)
|
|
norm_cross_check_double_nan = cross_check_nan_bytes.unpack("G").first
|
|
out "- double NaN payload normalized to 1.0..2.0, via pack('G')", norm_cross_check_double_nan
|
|
|
|
# Breakage 1: unpack("g") always sets the quiet bit to 1 in the Float result
|
|
|
|
unpacked_single_nan = as_bytes.unpack("g").first # broken -- sets quiet bit to 1
|
|
double_packed_unpacked_single_nan = [unpacked_single_nan].pack("G")
|
|
|
|
out "unpacked_single [unpack('g')].pack('G') output always quiet", unpacked_single_nan, double_packed_unpacked_single_nan.hexs
|
|
|
|
if double_packed_unpacked_single_nan != correct_double_bytes
|
|
out "- *** quieted value differs", double_packed_unpacked_single_nan.hexs, correct_double_bytes.hexs
|
|
end
|
|
|
|
# Breakage 2: pack("g") completely loses any NaN input and always packs the same bytes for a NaN
|
|
|
|
single_packed_unpacked_single_nan = [unpacked_single_nan].pack("g")
|
|
|
|
err = "for once, correct single NaN"
|
|
if single_packed_unpacked_single_nan != as_bytes
|
|
err = "*** the lost NaN -- output always the same"
|
|
end
|
|
|
|
out "#{err}: [unpack('g')].pack('g')", unpacked_single_nan, single_packed_unpacked_single_nan.hexs
|
|
puts
|
|
end
|