git.delta.rocks / jrsonnet / refs/commits / 228607312c2f

difftreelog

fix float formatting precision for values > u64::MAX

mqpuzzvrYaroslav Bolyukin2026-06-18parent: #7df0eec.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
47 "jrsonnet-ir-parser?/exp-object-iteration",47 "jrsonnet-ir-parser?/exp-object-iteration",
48]48]
49# Bigint type49# Bigint type
50exp-bigint = ["num-bigint", "jrsonnet-types/exp-bigint"]50exp-bigint = ["jrsonnet-types/exp-bigint"]
51# obj?.field, obj?.['field']51# obj?.field, obj?.['field']
52exp-null-coaelse = [52exp-null-coaelse = [
53 "jrsonnet-peg-parser?/exp-null-coaelse",53 "jrsonnet-peg-parser?/exp-null-coaelse",
78# Better explaining traces78# Better explaining traces
79hi-doc = { workspace = true, optional = true }79hi-doc = { workspace = true, optional = true }
80# Bigint80# Bigint
81num-bigint = { workspace = true, features = ["serde"], optional = true }81num-bigint = { workspace = true, features = ["serde"] }
8282
83stacker.workspace = true83stacker.workspace = true
84educe = { workspace = true, features = [84educe = { workspace = true, features = [
modifiedcrates/jrsonnet-evaluator/src/stdlib/format.rsdiffbeforeafterboth
6 reason = "many safe integer casts, behavior on overflow is not specified"6 reason = "many safe integer casts, behavior on overflow is not specified"
7)]7)]
8
9use std::fmt::Write as _;
810
9use jrsonnet_gcmodule::Trace;11use jrsonnet_gcmodule::Trace;
10use jrsonnet_interner::IStr;12use jrsonnet_interner::IStr;
11use jrsonnet_types::ValType;13use jrsonnet_types::ValType;
14use num_bigint::BigUint;
12use thiserror::Error;15use thiserror::Error;
1316
14use crate::{17use crate::{
300303
301const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";304const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
305
306#[allow(clippy::cast_precision_loss, reason = "fits exactly")]
307const TWO_POW_64: f64 = (1u128 << 64) as f64;
302308
303#[inline]309#[inline]
304#[allow(clippy::fn_params_excessive_bools)]310#[allow(clippy::fn_params_excessive_bools)]
316 caps: bool,322 caps: bool,
317) {323) {
318 debug_assert!(iv >= 0.0, "render_integer receives sign using arg");324 debug_assert!(iv >= 0.0, "render_integer receives sign using arg");
319 let iv = iv.floor() as i64;325 let iv = iv.floor();
320 // Digit char indexes in reverse order, i.e326 // Digit char indexes in reverse order, i.e
321 // for radix = 16 and n = 12f: [15, 2, 1]327 // for radix = 16 and n = 12f: [15, 2, 1]
322 let digits = if iv == 0 {328 let digits = if iv == 0.0 {
323 vec![0u8]329 vec![0u8]
324 } else {330 } else if iv < TWO_POW_64 {
325 let mut v = iv.abs();331 let mut v = iv as u64;
332 let radix = radix as u64;
326 let mut nums = Vec::with_capacity(1);333 let mut nums = Vec::with_capacity(1);
327 while v != 0 {334 while v != 0 {
328 nums.push((v % radix) as u8);335 nums.push((v % radix) as u8);
329 v /= radix;336 v /= radix;
330 }337 }
331 nums338 nums
332 };339 } else {
340 let (mantissa, exp) = decompose_f64(iv);
341 let mut v = BigUint::from(mantissa) << u32::try_from(exp).expect("huge f64 exp fits u32");
342 let radix = BigUint::from(radix as u64);
343 let mut nums = Vec::new();
344 while v != BigUint::ZERO {
345 nums.push(u8::try_from(&v % &radix).expect("digit < radix"));
346 v /= &radix;
347 }
348 nums
349 };
333 #[allow(clippy::bool_to_int_with_if)]350 #[allow(clippy::bool_to_int_with_if)]
334 let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });351 let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });
335352
348 }365 }
349366
350 out.reserve(zp2 as usize);367 out.reserve(zp2 as usize);
351 if iv != 0 {368 if iv != 0.0 {
352 out.push_str(zero_prefix);369 out.push_str(zero_prefix);
353 }370 }
354 for _ in 0..zp2 {371 for _ in 0..zp2 {
441 ensure_pt: bool,458 ensure_pt: bool,
442 trailing: bool,459 trailing: bool,
443) {460) {
444 // Represent the rounded number as an integer * 1/10**prec.
445 // Note that it can also be equal to 10**prec and we'll need to carry
446 // over to the wholes. We operate on the absolute numbers, so that we
447 // don't have trouble with the rounding direction.
448 let denominator = 10.0f64.powi(i32::from(precision));
449 let numerator = n.abs().mul_add(denominator, 0.5);461 let abs = n.abs();
450 let whole = (numerator / denominator).floor();462 let (frac, carry) = round_frac(abs, precision);
451 let frac = numerator.floor() % denominator;463 let whole = if carry {
464 abs.floor() + 1.0
465 } else {
466 abs.floor()
467 };
452468
453 #[allow(clippy::bool_to_int_with_if)]469 #[allow(clippy::bool_to_int_with_if)]
454 let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };470 let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };
460 }477 }
461 return;478 return;
462 }479 }
463 if trailing || frac > 0.0 {480
481 // For %g (trailing == false) trailing zeros are then trimmed in place
482 let dot_at = out.len();
464 out.push('.');483 out.push('.');
465 let mut frac_str = String::new();484 let digits_at = out.len();
466 render_decimal(&mut frac_str, false, frac, precision, 0, false, false);485 frac.write_padded(out, precision);
486 if !trailing {
467 let mut trim = frac_str.len();487 let kept = digits_at + out[digits_at..].trim_end_matches('0').len();
468 if !trailing {
469 for b in frac_str.as_bytes().iter().rev() {
470 if *b == b'0' {
471 trim -= 1;
472 } else {
473 break;
474 }
475 }
476 }
477 out.push_str(&frac_str[..trim]);488 out.truncate(kept);
478 } else if ensure_pt {489 if out.len() == digits_at && !ensure_pt {
479 out.push('.');490 out.truncate(dot_at);
480 }491 }
492 }
481}493}
494
495fn decompose_f64(n: f64) -> (u64, i32) {
496 const BITS: u32 = f64::MANTISSA_DIGITS - 1;
497 const BIAS: i32 = 1023;
498
499 let pattern = n.to_bits();
500 let exp_field = ((pattern >> BITS) & 0x7ff) as i32;
501 let mantissa = pattern & ((1u64 << BITS) - 1);
502 if exp_field == 0 {
503 (mantissa, 1 - BIAS - BITS.cast_signed())
504 } else {
505 (
506 mantissa | (1u64 << BITS),
507 exp_field - BIAS - BITS.cast_signed(),
508 )
509 }
510}
511
512enum Frac {
513 Small(u128),
514 Big(BigUint),
515}
516impl Frac {
517 fn write_padded(&self, out: &mut String, width: u16) {
518 let width = usize::from(width);
519 match self {
520 Self::Small(v) => write!(out, "{v:0width$}"),
521 Self::Big(v) => write!(out, "{v:0width$}"),
522 }
523 .expect("writing to a String never fails");
524 }
525}
526
527/// bool signifies carry, e.g 0.999 at precision 2 results in 1.000, thus carry.
528// floor((2 * low_bits * 10**prec + 2**k) / 2**(k+1))
529fn round_frac(abs: f64, precision: u16) -> (Frac, bool) {
530 let prec = usize::from(precision);
531 let (mantissa, exp) = decompose_f64(abs);
532 if exp >= 0 {
533 // abs is integral: no fractional bits.
534 return (Frac::Small(0), false);
535 }
536 let k = u32::try_from(-exp).expect("exp fits u32");
537 // Fractional part is exactly `low_bits / 2**k` (mantissa is 53 bits).
538 let low_bits = if k >= 64 {
539 mantissa
540 } else {
541 mantissa & ((1u64 << k) - 1)
542 };
543 if low_bits == 0 {
544 return (Frac::Small(0), false);
545 }
546 round_frac_u128(low_bits, k, prec).unwrap_or_else(|| round_frac_big(low_bits, k, prec))
547}
548
549fn round_frac_u128(low_bits: u64, k: u32, prec: usize) -> Option<(Frac, bool)> {
550 if k > 126 {
551 return None;
552 }
553 let mut ten_pow = 1u128;
554 for _ in 0..prec {
555 ten_pow = ten_pow.checked_mul(10)?;
556 }
557 let numerator = u128::from(low_bits)
558 .checked_mul(ten_pow)?
559 .checked_mul(2)?
560 .checked_add(1u128 << k)?;
561 let frac = numerator >> (k + 1);
562 Some(if frac == ten_pow {
563 (Frac::Small(0), true)
564 } else {
565 (Frac::Small(frac), false)
566 })
567}
568
569fn round_frac_big(low_bits: u64, k: u32, prec: usize) -> (Frac, bool) {
570 let mut ten_pow = BigUint::from(1u32);
571 let ten = BigUint::from(10u32);
572 for _ in 0..prec {
573 ten_pow *= &ten;
574 }
575 let numerator = ((BigUint::from(low_bits) * &ten_pow) << 1u32) + (BigUint::from(1u32) << k);
576 let frac = numerator >> (k + 1);
577 if frac == ten_pow {
578 (Frac::Small(0), true)
579 } else {
580 (Frac::Big(frac), false)
581 }
582}
482583
483#[allow(clippy::fn_params_excessive_bools)]584#[allow(clippy::fn_params_excessive_bools)]
484pub fn render_float_sci(585pub fn render_float_sci(