// SPDX-License-Identifier: GPL-2.0-only
// Port of boofuzz/primitives/float.py at 518c13904fc32e7f2cc88c9dec934e509062953e.
// Upstream with seed=None relies on the process-global RNG (not reproducible);
// the port fixes the sequence seed at 0 in that case so indexed candidate
// access stays positionally stable. Only the CPython "%.Nf" format subset is
// supported ("f" defaults to six digits like "%f"), rendered with exact
// decimal rounding of the double, matching CPython printf output.
///|
/// Float candidates: the first mutation is the default value formatted with
/// s_format, the rest draw random.uniform(f_min, f_max) from the seeded
/// MT19937 stream; adjacent duplicates are dropped exactly like upstream,
/// where the uniform draw is consumed before the dedup check so the
/// remaining sequence matches upstream's generator. The candidate count is
/// the number of formatted values actually produced, whereas upstream
/// num_mutations keeps claiming max_mutations.
pub fn Field::float(
default_value? : Double = 0.0,
s_format? : String = ".1f",
f_min? : Double = 0.0,
f_max? : Double = 100.0,
max_mutations? : Int = 1000,
seed? : Int64,
encode_as_ieee_754? : Bool = false,
endian? : Endian = Big,
fuzzable? : Bool = true,
fuzz_values? : Array[Bytes] = [],
) -> Field raise ModelError {
guard max_mutations >= 0 else { raise Invalid("negative max_mutations") }
guard f_min <= f_max else { raise Invalid("float range: f_min > f_max") }
let precision = parse_float_precision(s_format)
let encode = fn(text : String) -> Bytes raise ModelError {
if encode_as_ieee_754 {
encode_float_ieee_754(parse_decimal(text), endian)
} else {
ascii_bytes(text)
}
}
let value = encode(python_str_double(default_value))
let generator = Mt19937::from_seed(seed.unwrap_or(0L))
let saved : Array[Bytes] = []
let mut last : String? = None
for i in 0.. saved[i].length().to_int64(),
}
Field::with_fuzz_values(base, fuzz_values, fuzzable)
}
///|
/// Accept only Python "%[.N]f" specifiers: bare "f" (CPython's "%f" default
/// precision of 6), ".f" (precision 0) and ".Nf". Digits without a leading
/// dot are width/flag characters in CPython, never precision, so they are
/// rejected instead of misread.
fn parse_float_precision(s_format : String) -> Int raise ModelError {
guard s_format.has_suffix("f") else {
raise Invalid("float format must be %.Nf")
}
if !s_format.has_prefix(".") {
guard s_format == "f" else { raise Invalid("float format must be %.Nf") }
return 6
}
let mut precision = 0
for i in 1..<(s_format.length() - 1) {
let code = s_format[i].to_int() - 48
guard code >= 0 && code <= 9 else {
raise Invalid("float format must be %.Nf")
}
precision = precision * 10 + code
// Keep absurd precision specs from ballooning the exact-arithmetic
// workload; CPython has no limit but a fuzz payload never needs it.
guard precision <= 1_000_000 else {
raise Invalid("float format precision too large")
}
}
precision
}
///|
fn double_infinity() -> Double {
0x7ff0000000000000UL.reinterpret_as_double()
}
///|
fn double_nan() -> Double {
0x7ff8000000000000UL.reinterpret_as_double()
}
///|
/// CPython str(float): integral values print with a trailing ".0".
fn python_str_double(value : Double) -> String {
let bits = value.reinterpret_as_uint64()
if bits << 1 == 0UL {
return if bits >> 63 == 1UL { "-0.0" } else { "0.0" }
}
if ((bits >> 52) & 0x7ffUL) == 0x7ffUL {
return if bits << 12 != 0UL {
"nan"
} else if bits >> 63 == 1UL {
"-inf"
} else {
"inf"
}
}
let text = value.to_string()
if text.contains(".") || text.contains("e") || text.contains("E") {
text
} else {
text + ".0"
}
}
///|
/// "%.Nf" fixed-point formatting with CPython printf semantics: the exact
/// binary value of the double is rounded to N decimal digits exactly once,
/// round-half-to-even on the exact value. Midpoints such as 0.05 with
/// "%.1f" therefore render like CPython ("0.1": the double is strictly
/// above the decimal midpoint) instead of double rounding through a scaled
/// double, which would print "0.0".
fn format_fixed_point(value : Double, precision : Int) -> String {
let bits = value.reinterpret_as_uint64()
let zeros = if precision > 0 { "." + "0".repeat(precision) } else { "" }
if bits << 1 == 0UL {
return if bits >> 63 == 1UL { "-0" + zeros } else { "0" + zeros }
}
if ((bits >> 52) & 0x7ffUL) == 0x7ffUL {
if bits << 12 != 0UL {
return if bits >> 63 == 1UL { "-nan" } else { "nan" }
}
return if bits >> 63 == 1UL { "-inf" } else { "inf" }
}
let negative = bits >> 63 == 1UL
let magnitude = if negative { -value } else { value }
// magnitude = m * 2^k with m a 53-bit integer (subnormals keep their
// raw fraction and the minimum exponent).
let raw = magnitude.reinterpret_as_uint64()
let biased = ((raw >> 52) & 0x7ffUL).to_int()
let (m, k) = if biased == 0 {
(raw & 0xfffffffffffffUL, -1074)
} else {
((raw & 0xfffffffffffffUL) | 0x10000000000000UL, biased - 1075)
}
// digits = round_half_even(m * 10^N * 2^k), computed exactly.
let scaled = big_from_u64(m)
for _ in 0..= 0 {
big_shl(scaled, k)
} else {
let j = -k
// Divisor 2^j: r > half iff any bit below j-1 is set; r == half iff
// bit j-1 is set and no lower bit is. Ties round to even, so the tie
// bit alone rounds up only when the quotient (bit j of the scaled
// value) is odd.
let round_up = big_bit(scaled, j - 1) &&
(big_any_bit_below(scaled, j - 1) || big_bit(scaled, j))
big_shr(scaled, j)
if round_up {
big_add_one(scaled)
}
}
let digits = big_to_decimal(scaled)
let body = if precision == 0 {
digits
} else {
let padded = if digits.length() <= precision {
"0".repeat(precision + 1 - digits.length()) + digits
} else {
digits
}
let cut = padded.length() - precision
padded[:cut].to_owned() + "." + padded[cut:].to_owned()
}
if negative {
"-" + body
} else {
body
}
}
///|
/// Little-endian base-2^32 limb scratch values for the exact %.Nf
/// rounding above; magnitudes stay under ~1200 bits, so small arrays
/// cover every double.
fn big_from_u64(v : UInt64) -> Array[UInt] {
[v.to_uint(), (v >> 32).to_uint()]
}
///|
fn big_mul_small(a : Array[UInt], m : UInt) -> Unit {
let mut carry : UInt64 = 0UL
for i in 0..> 32
}
if carry != 0UL {
a.push(carry.to_uint())
}
}
///|
fn big_bit(a : Array[UInt], i : Int) -> Bool {
let limb = i / 32
if limb >= a.length() {
return false
}
((a[limb] >> (i % 32)) & 1U) == 1U
}
///|
/// Whether any bit strictly below position i is set.
fn big_any_bit_below(a : Array[UInt], i : Int) -> Bool {
let limb = i / 32
for j in 0.. Unit {
if bits == 0 {
return
}
let words = bits / 32
let off = bits % 32
let n = a.length()
for _ in 0..<(words + 1) {
a.push(0U)
}
let total = a.length()
if off == 0 {
for i = total - 1; i >= 0; i = i - 1 {
a[i] = if i - words >= 0 && i - words < n { a[i - words] } else { 0U }
}
return
}
// Written right to left so the sources are never overwritten first.
for i = total - 1; i >= 0; i = i - 1 {
let src = i - words
let mut v : UInt64 = 0UL
if src >= 1 && src - 1 < n {
v = a[src - 1].to_uint64() >> (32 - off)
}
if src >= 0 && src < n {
v = v | (a[src].to_uint64() << off)
}
a[i] = v.to_uint()
}
}
///|
fn big_shr(a : Array[UInt], bits : Int) -> Unit {
let words = bits / 32
let off = bits % 32
let n = a.length()
if words >= n {
for i in 0..> off).to_uint()
if i + words + 1 < n {
v = v | (a[i + words + 1].to_uint64() << (32 - off)).to_uint()
}
a[i] = v
}
}
for i in (n - words).. Unit {
for i in 0.. String {
let mut n = a.length()
while n > 1 && a[n - 1] == 0U {
n -= 1
}
if n == 1 && a[0] == 0U {
return "0"
}
let groups : Array[String] = []
while n > 1 || a[n - 1] != 0U {
let mut rem : UInt64 = 0UL
for i = n - 1; i >= 0; i = i - 1 {
let cur = (rem << 32) | a[i].to_uint64()
a[i] = (cur / 1000000000UL).to_uint()
rem = cur % 1000000000UL
}
groups.push(rem.to_string())
while n > 1 && a[n - 1] == 0U {
n -= 1
}
}
let mut text = groups[groups.length() - 1]
for i = groups.length() - 2; i >= 0; i = i - 1 {
text = text + "0".repeat(9 - groups[i].length()) + groups[i]
}
text
}
///|
/// Correctly-rounded decimal parse for the "%.Nf" output subset plus the
/// inf/nan spellings that survive Python's float(value) round trip.
/// The full digit string is accumulated as an integer and divided once, so
/// the result matches CPython's strtod for the short literals produced by
/// fixed-point formatting.
fn parse_decimal(text : String) -> Double raise ModelError {
if text == "inf" {
return double_infinity()
}
if text == "nan" {
return double_nan()
}
let negative = text.has_prefix("-")
let mut mantissa : Int64 = 0L
let mut fraction_digits = 0
let mut seen_dot = false
let mut seen_digit = false
for i in 0..= 0 && digit <= 9 else { raise Invalid("bad float literal") }
seen_digit = true
guard mantissa < 922337203685477580L else {
raise Invalid("float literal exceeds precision support")
}
mantissa = mantissa * 10L + digit.to_int64()
if seen_dot {
fraction_digits += 1
}
}
guard seen_digit else { raise Invalid("bad float literal") }
let mut denominator = 1.0
for _ in 0.. Bytes {
let bytes : Array[Byte] = []
for ch in text {
let code = ch.to_int()
guard code >= 0 && code <= 127 else { continue }
bytes.push(code.to_byte())
}
Bytes::from_array(bytes)
}
///|
/// Pack a Double as IEEE 754 binary32 with round-to-nearest-even, matching
/// the C double-to-float conversion used by struct.pack(">f"/" Bytes {
let bits = value.reinterpret_as_uint64()
let sign = (bits >> 63 << 31).to_uint() & 0x80000000U
let exponent = ((bits >> 52) & 0x7ffUL).to_int()
let mantissa = bits & 0xfffffffffffffUL
let packed : UInt = if exponent == 0x7ff {
if mantissa == 0UL {
sign | 0x7f800000U
} else {
sign | 0x7fc00000U
}
} else if exponent == 0 && mantissa == 0UL {
sign
} else {
// Full significand with the implicit bit, aligned for a 23-bit field.
let full = mantissa | 0x10000000000000UL
let biased = exponent - 896
if biased >= 0xff {
sign | 0x7f800000U
} else if biased > 0 {
let dropped = full & 0x1fffffffUL
let mut kept = (full >> 29).to_uint()
let mut exp = biased.reinterpret_as_uint()
if dropped > 0x10000000UL ||
(dropped == 0x10000000UL && (kept & 1U) == 1U) {
kept += 1U
if kept == 0x800000U {
kept = 0U
exp += 1U
}
}
sign | (exp << 23) | (kept & 0x7fffffU)
} else {
// Subnormal or zero in binary32.
let shift = 926 - exponent
let mut kept : UInt = 0U
if shift <= 53 {
let quotient = full >> shift
let remainder = full & ((1UL << shift) - 1UL)
let half = 1UL << (shift - 1)
let mut result = quotient.to_uint()
if remainder > half || (remainder == half && (quotient & 1UL) == 1UL) {
result += 1U
}
if result == 0x800000U {
// Rounded up into the smallest normal exponent.
return encode_integer((sign | (1U << 23)).to_uint64(), 32, endian)
}
kept = result
}
sign | kept
}
}
encode_integer(packed.to_uint64(), 32, endian)
}